KiCad PCB EDA Suite
Loading...
Searching...
No Matches
kigit_pcb_merge.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, see <https://www.gnu.org/licenses/>.
18 */
19
20#include "kigit_pcb_merge.h"
22
26
27#include <advanced_config.h>
28#include <board.h>
32#include <richio.h>
33#include <trace_helpers.h>
34
35#include <wx/log.h>
36
37void KIGIT_PCB_MERGE::findSetDifferences( const BOARD_ITEM_SET& aAncestorSet, const BOARD_ITEM_SET& aOtherSet,
38 std::vector<BOARD_ITEM*>& aAdded, std::vector<BOARD_ITEM*>& aRemoved,
39 std::vector<BOARD_ITEM*>& aChanged )
40{
41 auto it1 = aAncestorSet.begin();
42 auto it2 = aOtherSet.begin();
43
44 while( it1 != aAncestorSet.end() && it2 != aOtherSet.end() )
45 {
46 BOARD_ITEM* item1 = *it1;
47 BOARD_ITEM* item2 = *it2;
48
49 if( item1->m_Uuid < item2->m_Uuid )
50 {
51 aRemoved.push_back( item1 );
52 ++it1;
53 }
54 else if( item1->m_Uuid > item2->m_Uuid )
55 {
56 aAdded.push_back( item2 );
57 ++it2;
58 }
59 else
60 {
61 if( !( *item1 == *item2 ) )
62 aChanged.push_back( item1 );
63
64 ++it1;
65 ++it2;
66 }
67 }
68}
69
70
72{
74
75 const auto ancestor_set = aAncestor->GetItemSet();
76 const auto other_set = aOther->GetItemSet();
77
78 findSetDifferences( ancestor_set, other_set, differences.m_added, differences.m_removed, differences.m_changed );
79
80 return differences;
81}
82
83
84std::string KIGIT_PCB_MERGE::FormatBoard( BOARD* aBoard )
85{
86 STRING_FORMATTER formatter;
88
89 pcbIO.FormatBoardToFormatter( &formatter, aBoard );
90
91 std::string text = std::move( formatter.MutableString() );
92
94 ? KICAD_FORMAT::FORMAT_MODE::COMPACT_TEXT_PROPERTIES
95 : KICAD_FORMAT::FORMAT_MODE::NORMAL );
96 return text;
97}
98
99
101{
102 // The structural 3-way merge needs all three sides. A null ancestor entry
103 // means one side added a file that doesn't exist on the other; without a
104 // common ancestor there is nothing to diff against, so decline and let
105 // libgit2 fall back to its default handling. LoadMergeBlobs tolerates a
106 // missing ancestor, so this guard is kept explicit to preserve behavior.
107 if( !git_merge_driver_source_ancestor( m_mergeDriver ) )
108 {
109 wxLogTrace( traceGit, "PCB merge driver: no common ancestor, declining merge" );
110 return GIT_PASSTHROUGH;
111 }
112
113 KIGIT::MERGE_BLOBS blobs;
114
115 if( int rc = KIGIT::LoadMergeBlobs( m_mergeDriver, blobs ); rc != 0 )
116 {
117 wxLogTrace( traceGit, "PCB merge driver: could not load merge blobs (rc=%d)", rc );
118 return rc;
119 }
120
121 // Parse each side from its decoded blob contents. LoadMergeBlobs has freed
122 // the underlying git_blob objects, so the boards are built from the strings.
123 STRING_LINE_READER ancestor_reader( blobs.ancestor, wxS( "ancestor" ) );
124 PCB_IO_KICAD_SEXPR_PARSER ancestor_parser( &ancestor_reader, nullptr, nullptr );
125 STRING_LINE_READER ours_reader( blobs.ours, wxS( "ours" ) );
126 PCB_IO_KICAD_SEXPR_PARSER ours_parser( &ours_reader, nullptr, nullptr );
127 STRING_LINE_READER theirs_reader( blobs.theirs, wxS( "theirs" ) );
128 PCB_IO_KICAD_SEXPR_PARSER theirs_parser( &theirs_reader, nullptr, nullptr );
129
130 std::unique_ptr<BOARD> ancestor_board;
131 std::unique_ptr<BOARD> ours_board;
132 std::unique_ptr<BOARD> theirs_board;
133
134 try
135 {
136 ancestor_board.reset( static_cast<BOARD*>( ancestor_parser.Parse() ) );
137 ours_board.reset( static_cast<BOARD*>( ours_parser.Parse() ) );
138 theirs_board.reset( static_cast<BOARD*>( theirs_parser.Parse() ) );
139 }
140 catch(const IO_ERROR& e)
141 {
142 wxLogTrace( traceGit, "Could not parse board: %s", e.What() );
143 return GIT_EUSER;
144 }
145 catch( ... )
146 {
147 wxLogTrace( traceGit, "Could not parse board: unknown error" );
148 return GIT_EUSER;
149 }
150
151 // Keep the legacy difference sets populated for any external consumer
152 // (none today, but the accessors are public API). The full 3-way merge
153 // logic runs through the new diff/merge engine.
154 KIGIT_PCB_MERGE_DIFFERENCES ancestor_ours_differences =
155 compareBoards( ancestor_board.get(), ours_board.get() );
156 KIGIT_PCB_MERGE_DIFFERENCES ancestor_theirs_differences =
157 compareBoards( ancestor_board.get(), theirs_board.get() );
158
159 std::set_intersection( ancestor_ours_differences.m_changed.begin(),
160 ancestor_ours_differences.m_changed.end(),
161 ancestor_theirs_differences.m_removed.begin(),
162 ancestor_theirs_differences.m_removed.end(),
163 std::inserter( we_modified_they_deleted, we_modified_they_deleted.begin() ) );
164 std::set_intersection( ancestor_theirs_differences.m_changed.begin(),
165 ancestor_theirs_differences.m_changed.end(),
166 ancestor_ours_differences.m_removed.begin(),
167 ancestor_ours_differences.m_removed.end(),
168 std::inserter( they_modified_we_deleted, they_modified_we_deleted.begin() ) );
169 std::set_intersection( ancestor_ours_differences.m_changed.begin(),
170 ancestor_ours_differences.m_changed.end(),
171 ancestor_theirs_differences.m_changed.begin(),
172 ancestor_theirs_differences.m_changed.end(),
173 std::inserter( both_modified, both_modified.begin() ) );
174
175 // Run the structured diff/merge pipeline so we can actually emit a
176 // merged blob into git's buffer. Project pointers from the parser
177 // belong to whichever PROJECT the loader supplied — for blob loads
178 // there is no project, so this is safe.
179 KICAD_DIFF::PCB_DIFFER ourDiffer( ancestor_board.get(), ours_board.get() );
180 KICAD_DIFF::PCB_DIFFER theirDiffer( ancestor_board.get(), theirs_board.get() );
181
183 KICAD_DIFF::MERGE_PLAN plan = engine.Plan( ourDiffer.Diff(), theirDiffer.Diff() );
184
185 // Captured before the plan is moved into the applier; drives the
186 // conflict-or-clean return below without a second Plan() recompute.
187 const bool planResolved = plan.Resolved();
188
189 KICAD_DIFF::PCB_MERGE_APPLIER applier( ancestor_board.get(), ours_board.get(),
190 theirs_board.get(), std::move( plan ) );
191 std::unique_ptr<BOARD> merged = applier.Apply();
192
193 if( !merged )
194 {
195 wxLogTrace( traceGit, "PCB merge applier failed to produce a board" );
196 return GIT_EUSER;
197 }
198
199 // Serialize the merged board into a std::string via PCB_IO_KICAD_SEXPR.
200 std::string serialized;
201
202 try
203 {
204 serialized = FormatBoard( merged.get() );
205 }
206 catch( const IO_ERROR& ioe )
207 {
208 wxLogTrace( traceGit, "Failed to serialize merged board: %s", ioe.What() );
209 return GIT_EUSER;
210 }
211
212 if( KIGIT::WriteToGitBuf( m_result, serialized ) != 0 )
213 {
214 wxLogTrace( traceGit, "Failed to allocate git_buf for merged board" );
215 return GIT_EUSER;
216 }
217
218 // The merged board is fully serialized above, so the working-tree file is
219 // always valid. If conflicts remained unresolved, tell libgit2 to record a
220 // conflict; the user resolves it via the mergetool, which recomputes the
221 // plan from ancestor/ours/theirs.
222 if( !planResolved )
223 return GIT_EMERGECONFLICT;
224
225 return 0;
226}
227
228
234int KIGIT_PCB_MERGE::Apply( const git_merge_driver_source* aSrc, const char** aPathOut,
235 unsigned int* aModeOut, git_buf* aMergedOut )
236{
237 return KIGIT::ApplyMergeDriver<KIGIT_PCB_MERGE>( aSrc, aPathOut, aModeOut, aMergedOut );
238}
std::set< BOARD_ITEM *, CompareByUuid > BOARD_ITEM_SET
Set of BOARD_ITEMs ordered by UUID.
Definition board.h:395
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:85
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
const BOARD_ITEM_SET GetItemSet()
Collect every owned item (tracks, zones, generators, footprints, drawings, markers,...
Definition board.cpp:4214
const KIID m_Uuid
Definition eda_item.h:599
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
Three-way merge plan generator.
MERGE_PLAN Plan(const DOCUMENT_DIFF &aAncestorOurs, const DOCUMENT_DIFF &aAncestorTheirs) const
Plan the merge given the canonical pair of diffs.
Diff two already-parsed BOARDs and produce a DOCUMENT_DIFF.
Definition pcb_differ.h:62
DOCUMENT_DIFF Diff() override
Produce a DOCUMENT_DIFF of the inputs the concrete differ was constructed with.
Materialize a MERGE_PLAN into a real merged BOARD.
std::unique_ptr< BOARD > Apply()
Produce the merged board.
std::set< BOARD_ITEM * > both_modified
void findSetDifferences(const BOARD_ITEM_SET &aAncestorSet, const BOARD_ITEM_SET &aOtherSet, std::vector< BOARD_ITEM * > &aAdded, std::vector< BOARD_ITEM * > &aRemoved, std::vector< BOARD_ITEM * > &aChanged)
git_merge_driver_source * m_mergeDriver
KIGIT_PCB_MERGE_DIFFERENCES compareBoards(BOARD *aAncestor, BOARD *aOther)
std::set< BOARD_ITEM * > they_modified_we_deleted
static int Apply(const git_merge_driver_source *aSrc, const char **aPathOut, unsigned int *aModeOut, git_buf *aMergedOut)
libgit2 merge-driver apply callback shim.
static std::string FormatBoard(BOARD *aBoard)
Serialize a board to a complete, prettified .kicad_pcb file image.
std::set< BOARD_ITEM * > we_modified_they_deleted
Read a Pcbnew s-expression formatted LINE_READER object and returns the appropriate BOARD_ITEM object...
A PCB_IO derivation for saving and loading Pcbnew s-expression formatted files.
void FormatBoardToFormatter(OUTPUTFORMATTER *aOut, BOARD *aBoard, const std::map< std::string, UTF8 > *aProperties=nullptr)
Serialize a BOARD to an OUTPUTFORMATTER without file I/O or Prettify.
Implement an OUTPUTFORMATTER to a memory buffer.
Definition richio.h:430
std::string & MutableString()
Definition richio.h:458
Is a LINE_READER that reads from a multiline 8 bit wide std::string.
Definition richio.h:225
const wxChar *const traceGit
Flag to enable Git debugging output.
static std::vector< PENDING_PROPERTY > plan(const EDA_ITEM &aSource, const EDA_ITEM &aTarget, const std::set< wxString > &aEnabledKeys)
Target properties to write, paired with values read off the source.
void Prettify(std::string &aSource, FORMAT_MODE aMode)
Pretty-prints s-expression text according to KiCad format rules.
int WriteToGitBuf(git_buf *aBuf, const std::string &aContent)
Allocate a libgit2-owned buffer big enough for aContent and copy the bytes plus a trailing NUL.
int LoadMergeBlobs(const git_merge_driver_source *aSource, MERGE_BLOBS &aBlobs)
Look up the ancestor/ours/theirs blobs of a merge-driver source and decode them into aBlobs.
int ApplyMergeDriver(const git_merge_driver_source *aSrc, const char **aPathOut, unsigned int *aModeOut, git_buf *aMergedOut)
Shared libgit2 merge-driver apply callback shim.
Pcbnew s-expression file format parser definition.
Result of planning a 3-way merge.
Decoded ancestor/ours/theirs blob contents for a 3-way merge driver.
std::string ancestor
Empty when there is no common ancestor.
std::vector< BOARD_ITEM * > m_removed
std::vector< BOARD_ITEM * > m_changed
std::vector< BOARD_ITEM * > m_added
wxLogTrace helper definitions.