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 // Choices belong to this merge, so do not restore them from another dialog.
110 OptOut( this );
111
112 // Splice a GAL-backed conflict viewer into the resolution panel's sizer
113 // when one is present. The original .fbp emits a text-only layout; we
114 // augment it programmatically here rather than re-authoring the .fbp.
115 if( wxSizer* resolutionSizer = m_panelResolution->GetSizer() )
116 {
118 m_canvas->SetMinSize( wxSize( -1, 200 ) );
119
120 // Insert just above the text detail (which sits at index 1 after
121 // the label). Keep the existing layout otherwise intact.
122 const size_t insertAt = std::min<size_t>( 1, resolutionSizer->GetItemCount() );
123 resolutionSizer->Insert( insertAt, m_canvas,
124 wxSizerFlags( 1 ).Expand().Border( wxALL, 4 ) );
125 m_panelResolution->Layout();
126 }
127
128 if( !m_context.formatNotice.IsEmpty() )
129 m_labelIntro->SetLabel( m_labelIntro->GetLabel() + wxS( "\n\n" ) + m_context.formatNotice );
130
131 buildList();
132
133 if( m_conflictActionIndex.empty() )
134 {
135 m_labelDetail->SetLabel( _( "No conflicts to resolve." ) );
136 m_sdbSizerApply->Enable( true );
137 }
138 else
139 {
140 m_listConflicts->SetSelection( 0 );
141 showConflict( 0 );
142 }
143
145
146 Layout();
147
148 // Headless / scripted mode: if KICAD_MERGETOOL_AUTO is set, apply the
149 // pre-recorded resolutions and close the dialog immediately. The
150 // EndModal is deferred via CallAfter so it runs after ShowModal has
151 // started its event loop. Used by CI test runners that exercise the
152 // full git mergetool → kicad-cli → kicad --mergetool → applier flow
153 // without a human at the keyboard.
154 //
155 // Bad-env paths (missing file, parse error, partial coverage, unknown
156 // resolution kind) explicitly EndModal( wxID_CANCEL ) so the caller's
157 // exit code is non-zero. Silently falling back to the interactive
158 // dialog would hang a CI run forever.
159 std::map<wxString, KICAD_DIFF::ITEM_RES> autoResolutions;
160
161 switch( loadAutoResolutionFile( autoResolutions ) )
162 {
163 case AUTO_MODE::NOT_SET:
164 break;
165
166 case AUTO_MODE::LOAD_FAILED:
167 wxLogTrace( traceDiffMerge,
168 wxT( "KICAD_MERGETOOL_AUTO load failed; closing dialog as cancelled" ) );
169 CallAfter( [this]() { EndModal( wxID_CANCEL ); } );
170 break;
171
172 case AUTO_MODE::LOADED:
173 {
176 autoResolutions );
177
178 switch( applied.status )
179 {
181 wxLogTrace( traceDiffMerge,
182 wxT( "KICAD_MERGETOOL_AUTO missing resolution for %s" ),
183 applied.firstMissingId.AsString() );
184 CallAfter( [this]() { EndModal( wxID_CANCEL ); } );
185 break;
186
189 CallAfter( [this]() { EndModal( wxID_APPLY ); } );
190 wxLogTrace( traceDiffMerge,
191 wxT( "KICAD_MERGETOOL_AUTO applied %zu auto-resolutions" ),
192 applied.appliedCount );
193 break;
194 }
195
196 break;
197 }
198 }
199}
200
201
202void DIALOG_KICAD_MERGE_3WAY::OnClose( wxCloseEvent& aEvent )
203{
204 EndModal( wxID_CANCEL );
205}
206
207
208void DIALOG_KICAD_MERGE_3WAY::OnCancel( wxCommandEvent& aEvent )
209{
210 EndModal( wxID_CANCEL );
211}
212
213
214void DIALOG_KICAD_MERGE_3WAY::OnApply( wxCommandEvent& aEvent )
215{
216 // Every conflict needs an explicit choice before the plan can be applied.
217 const std::vector<KIID_PATH> stillUnresolved =
219
220 if( !stillUnresolved.empty() )
221 {
222 wxMessageBox( wxString::Format( wxPLURAL( "%zu conflict still needs a resolution. "
223 "Pick Ours, Theirs, or Ancestor for each one.",
224 "%zu conflicts still need a resolution. "
225 "Pick Ours, Theirs, or Ancestor for each one.",
226 stillUnresolved.size() ),
227 stillUnresolved.size() ),
228 _( "Resolve Merge Conflicts" ),
229 wxOK | wxICON_INFORMATION, this );
230 return;
231 }
232
233 m_plan.unresolved.clear();
234 EndModal( wxID_APPLY );
235}
236
237
239{
240 showConflict( m_listConflicts->GetSelection() );
241}
242
243
245{
246 if( m_currentConflict < 0
247 || m_currentConflict >= static_cast<int>( m_conflictActionIndex.size() ) )
248 {
249 return;
250 }
251
252 std::size_t actionIdx = m_conflictActionIndex[m_currentConflict];
253 KICAD_DIFF::ITEM_RESOLUTION& action = m_plan.actions[actionIdx];
254
255 if( m_radioOurs->GetValue() )
257 else if( m_radioTheirs->GetValue() )
259 else if( m_radioAncestor->GetValue() )
263
266}
267
268
270{
271 if( m_radioTheirs->GetValue() )
272 return SIDE::THEIRS;
273
274 if( m_radioAncestor->GetValue() )
275 return SIDE::ANCESTOR;
276
277 return SIDE::OURS;
278}
279
280
282{
283 if( !m_canvas )
284 return;
285
287 const std::map<KIID_PATH, BOX2I>* primaryBBoxes = nullptr;
288
289 switch( activeSide() )
290 {
291 case SIDE::OURS:
292 scene.referenceGeometry = m_context.oursGeometry;
293 primaryBBoxes = &m_context.oursBBoxes;
294 break;
295 case SIDE::THEIRS:
296 scene.referenceGeometry = m_context.theirsGeometry;
297 primaryBBoxes = &m_context.theirsBBoxes;
298 break;
299 case SIDE::ANCESTOR:
300 scene.referenceGeometry = m_context.ancestorGeometry;
301 primaryBBoxes = &m_context.ancestorBBoxes;
302 break;
303 }
304
305 static const std::map<KIID_PATH, BOX2I> emptyBBoxMap;
306
307 BOX2I conflictBBox;
308
309 if( m_currentConflict >= 0
310 && m_currentConflict < static_cast<int>( m_conflictActionIndex.size() ) )
311 {
312 const KICAD_DIFF::ITEM_RESOLUTION& action =
314
315 const std::optional<BOX2I> resolved =
317 action.id,
318 primaryBBoxes ? *primaryBBoxes : emptyBBoxMap,
319 m_context.oursBBoxes, m_context.theirsBBoxes,
320 m_context.ancestorBBoxes );
321
322 if( resolved.has_value() )
323 {
324 conflictBBox = *resolved;
325
327 shape.bbox = conflictBBox;
328 shape.color = highlightColor();
329 shape.changeId = action.id;
330 scene.conflictShapes.push_back( shape );
331 }
332 }
333
334 // documentBBox covers the side's geometry so the user can pan/zoom out
335 // to see context; the explicit ZoomToBBox below focuses the initial view
336 // on the conflict so it isn't lost in the wide-area auto-fit.
338
339 std::optional<KIID_PATH> highlight;
340
341 if( m_currentConflict >= 0 )
342 highlight = m_plan.actions[m_conflictActionIndex[m_currentConflict]].id;
343
344 m_canvas->SetScene( std::move( scene ) );
345
346 // The editor's painter draws the real items, so text reads as text. The canvas drops the
347 // flattened geometry once it has them, and keeps the conflict highlight from the scene.
348 if( m_context.drawSide )
349 m_context.drawSide( *m_canvas, activeSide(), highlight );
350
351 m_canvas->HighlightChange( highlight );
352
353 if( conflictBBox.GetWidth() > 0 || conflictBBox.GetHeight() > 0 )
354 {
355 // Pad the focus a bit so the highlight isn't flush with the edges
356 // and the user sees some surrounding context.
357 BOX2I focus = conflictBBox;
358 focus.Inflate( std::max( conflictBBox.GetWidth(), conflictBBox.GetHeight() ) );
359 m_canvas->ZoomToBBox( focus );
360 }
361}
362
363
364wxString DIALOG_KICAD_MERGE_3WAY::describe( const KIID_PATH& aId, const wxString& aFallback ) const
365{
366 if( m_context.describeItem )
367 {
368 wxString named = m_context.describeItem( aId );
369
370 if( !named.IsEmpty() )
371 return named;
372 }
373
374 return aFallback;
375}
376
377
379{
380 m_listConflicts->Clear();
381 m_conflictActionIndex.clear();
382 m_hasMergedEdit.clear();
383
384 for( const KICAD_DIFF::CONFLICT_LIST_ENTRY& entry
386 {
387 m_listConflicts->Append( describe( entry.id, entry.label ) );
388 m_conflictActionIndex.push_back( entry.actionIndex );
389 m_hasMergedEdit.push_back( m_plan.actions[entry.actionIndex].kind == KICAD_DIFF::ITEM_RES::MERGE_PROPS );
390 }
391}
392
393
395{
396 m_currentConflict = aIndex;
397
398 if( aIndex < 0 || aIndex >= static_cast<int>( m_conflictActionIndex.size() ) )
399 {
400 m_labelDetail->SetLabel( _( "Select a conflict on the left to see details." ) );
401 m_textDetail->SetValue( wxEmptyString );
402 return;
403 }
404
405 std::size_t actionIdx = m_conflictActionIndex[aIndex];
406 const KICAD_DIFF::ITEM_RESOLUTION& action = m_plan.actions[actionIdx];
407
408 const bool offersMerge = m_hasMergedEdit[aIndex];
409
410 // A native radio group cannot clear every button, so the fourth one holds whatever is
411 // not a side.
412 m_radioUnresolved->SetLabel( offersMerge ? _( "Merge both changes" ) : _( "Not decided yet" ) );
413 m_radioUnresolved->Enable( offersMerge );
414 m_panelResolution->Layout();
415
416 switch( action.kind )
417 {
418 case KICAD_DIFF::ITEM_RES::TAKE_OURS: m_radioOurs->SetValue( true ); break;
419 case KICAD_DIFF::ITEM_RES::TAKE_THEIRS: m_radioTheirs->SetValue( true ); break;
420 case KICAD_DIFF::ITEM_RES::TAKE_ANCESTOR: m_radioAncestor->SetValue( true ); break;
421 default: m_radioUnresolved->SetValue( true ); break;
422 }
423
424 // The canvas never draws the combined result, so say which document it is showing.
425 wxString shown;
426
427 switch( activeSide() )
428 {
429 case SIDE::OURS: shown = _( "showing your version" ); break;
430 case SIDE::THEIRS: shown = _( "showing their version" ); break;
431 case SIDE::ANCESTOR: shown = _( "showing the common version" ); break;
432 }
433
434 m_labelDetail->SetLabel(
435 wxString::Format( _( "Conflict on %s, %s" ), describe( action.id, action.id.AsString() ), shown ) );
436
438 {
439 // The detail box does not wrap, so the lines are broken here.
441 + _( "\n\nBoth sides changed this item, and their edits are combined.\n"
442 "Where both changed the same thing, your version is kept.\n"
443 "Pick a side to take that version whole instead." ) );
444 }
445 else if( m_radioUnresolved->GetValue() )
446 {
447 m_textDetail->SetValue( _( "Pick Ours, Theirs, or Ancestor to preview and keep that version." ) );
448 }
449 else
450 {
452 }
453
455}
BOX2< VECTOR2I > BOX2I
Definition box2.h:914
constexpr BOX2< Vec > & Inflate(coord_type dx, coord_type dy)
Inflates the rectangle horizontally by dx and vertically by dy.
Definition box2.h:553
constexpr size_type GetWidth() const
Definition box2.h:211
constexpr size_type GetHeight() const
Definition box2.h:212
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
SIDE activeSide() const
The side the canvas previews.
DIALOG_KICAD_MERGE_3WAY(wxWindow *aParent, const KICAD_DIFF::MERGE_PLAN &aPlan, CONFLICT_CONTEXT aContext={})
void OnApply(wxCommandEvent &aEvent) override
wxString describe(const KIID_PATH &aId, const wxString &aFallback) const
The item's name from the editor, or aFallback when there is none.
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 version the canvas should currently display.
std::vector< bool > m_hasMergedEdit
True where the engine combined both sides' edits, in the order of m_conflictActionIndex.
void OnResolutionChanged(wxCommandEvent &aEvent) override
std::vector< std::size_t > m_conflictActionIndex
Indices into m_plan.actions matching the order of m_listConflicts.
void OptOut(wxWindow *aWindow)
Opt out of control state saving.
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
wxString AsString() const
Definition kiid.cpp:423
GAL-backed canvas for visualizing a KICAD_DIFF::DIFF_SCENE.
static bool empty(const wxTextEntryBase *aCtrl)
#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 conflict actions that still have no decision.
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: the positions in plan....
Diff colors with defaults for light backgrounds.
Definition diff_scene.h:278
DOCUMENT_GEOMETRY referenceGeometry
Background geometry from the two source documents.
Definition diff_scene.h:242
std::vector< SCENE_SHAPE > conflictShapes
Definition diff_scene.h:236
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.