KiCad PCB EDA Suite
Loading...
Searching...
No Matches
dialog_git_mr_review.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
29#include <kiway.h>
30#include <reporter.h>
31
32#include <git2.h>
33#include <wx/file.h>
34#include <wx/filename.h>
35#include <wx/log.h>
36#include <wx/msgdlg.h>
37#include <wx/stdpaths.h>
38#include <wx/time.h>
39#include <wx/utils.h>
40
41
42DIALOG_GIT_MR_REVIEW::DIALOG_GIT_MR_REVIEW( wxWindow* aParent, git_repository* aRepo,
43 const std::vector<wxString>& aRefList ) :
45 m_repo( aRepo )
46{
47 m_listFiles->AppendColumn( _( "Path" ), wxLIST_FORMAT_LEFT, 400 );
48 m_listFiles->AppendColumn( _( "Status" ), wxLIST_FORMAT_LEFT, 100 );
49 m_listFiles->AppendColumn( _( "Old Path" ), wxLIST_FORMAT_LEFT, 200 );
50
51 for( const wxString& ref : aRefList )
52 {
53 m_comboBase->Append( ref );
54 m_comboHead->Append( ref );
55 }
56
57 if( !aRefList.empty() )
58 {
59 m_comboBase->SetSelection( 0 );
60 m_comboHead->SetSelection( aRefList.size() > 1 ? 1 : 0 );
61 }
62
63 Layout();
64}
65
66
67void DIALOG_GIT_MR_REVIEW::OnClose( wxCloseEvent& aEvent )
68{
69 EndModal( wxID_CANCEL );
70}
71
72
73void DIALOG_GIT_MR_REVIEW::OnCompareClick( wxCommandEvent& aEvent )
74{
76}
77
78
79void DIALOG_GIT_MR_REVIEW::OnFileActivated( wxListEvent& aEvent )
80{
81 openFileDiff( aEvent.GetIndex() );
82}
83
84
86{
87 m_listFiles->DeleteAllItems();
88 m_changedFiles.clear();
89
90 if( !m_repo )
91 {
92 wxMessageBox( _( "No git repository available." ), _( "Compare Branches" ),
93 wxOK | wxICON_ERROR, this );
94 return;
95 }
96
97 wxString base = m_comboBase->GetValue();
98 wxString head = m_comboHead->GetValue();
99
100 if( base.IsEmpty() || head.IsEmpty() )
101 {
102 wxMessageBox( _( "Select base and head refs to compare." ),
103 _( "Compare Branches" ), wxOK | wxICON_INFORMATION, this );
104 return;
105 }
106
107 // Validate each ref resolves and peels to a tree; CompareRefs needs a
108 // tree on both sides.
109 auto validateRef =
110 [this]( const wxString& aRef ) -> bool
111 {
113
114 if( !treePtr )
115 {
116 wxMessageBox(
117 wxString::Format( _( "Could not resolve ref '%s' to a commit or tree." ),
118 aRef ),
119 _( "Compare Branches" ), wxOK | wxICON_ERROR, this );
120 return false;
121 }
122
123 return true;
124 };
125
126 if( !validateRef( base ) || !validateRef( head ) )
127 return;
128
130
131 if( m_changedFiles.empty() )
132 {
133 m_listFiles->InsertItem( 0, _( "No changes between selected refs." ) );
134 return;
135 }
136
137 long row = 0;
138
139 for( const KIGIT::CHANGED_FILE& f : m_changedFiles )
140 {
141 long idx = m_listFiles->InsertItem( row++, f.path );
142 m_listFiles->SetItem( idx, 1,
143 wxString::FromUTF8( KIGIT::FileChangeStatusToString( f.status ) ) );
144
145 if( !f.oldPath.IsEmpty() )
146 m_listFiles->SetItem( idx, 2, f.oldPath );
147 }
148}
149
150
151namespace
152{
153
158bool writeRefBlobToFile( git_repository* aRepo, const wxString& aRef, const wxString& aPath,
159 const wxString& aDestPath )
160{
161 if( aRef.IsEmpty() || aPath.IsEmpty() )
162 return false;
163
164 KIGIT::GitTreePtr treePtr( KIGIT::ResolveRefToTree( aRepo, aRef ) );
165
166 if( !treePtr )
167 return false;
168
169 git_tree_entry* entry = nullptr;
170
171 if( git_tree_entry_bypath( &entry, treePtr.get(), aPath.ToUTF8().data() ) != 0 )
172 return false;
173
174 std::unique_ptr<git_tree_entry, decltype( &git_tree_entry_free )> entryPtr(
175 entry, &git_tree_entry_free );
176
177 if( git_tree_entry_type( entry ) != GIT_OBJECT_BLOB )
178 return false;
179
180 git_object* blobObj = nullptr;
181
182 if( git_tree_entry_to_object( &blobObj, aRepo, entry ) != 0 )
183 return false;
184
185 KIGIT::GitObjectPtr blobObjPtr( blobObj );
186 git_blob* blob = reinterpret_cast<git_blob*>( blobObj );
187
188 wxFile out( aDestPath, wxFile::write );
189
190 if( !out.IsOpened() )
191 return false;
192
193 const void* raw = git_blob_rawcontent( blob );
194 git_off_t size = git_blob_rawsize( blob );
195
196 return out.Write( raw, static_cast<size_t>( size ) ) == static_cast<size_t>( size );
197}
198
199} // namespace
200
201
202namespace
203{
204
208bool isPrettyFootprintChange( const wxString& aPath )
209{
211 return false;
212
213 wxFileName fn( aPath );
214 const wxArrayString& dirs = fn.GetDirs();
215
216 return !dirs.IsEmpty() && dirs.Last().EndsWith( wxS( ".pretty" ) );
217}
218
219
222struct TempDirGuard
223{
224 wxString path;
225
226 ~TempDirGuard()
227 {
228 if( !path.IsEmpty() )
229 wxFileName::Rmdir( path, wxPATH_RMDIR_RECURSIVE );
230 }
231};
232
233} // namespace
234
235
237{
238 if( aListIndex < 0 || aListIndex >= static_cast<long>( m_changedFiles.size() ) )
239 return;
240
241 const KIGIT::CHANGED_FILE& file = m_changedFiles[aListIndex];
242
244 bool prettyFootprint = isPrettyFootprintChange( file.path );
245
246 if( prettyFootprint )
248
250 {
251 wxMessageBox( wxString::Format( _( "No diff handler for '%s'." ), file.path ),
252 _( "Compare File" ), wxOK | wxICON_INFORMATION, this );
253 return;
254 }
255
256 const wxString base = m_comboBase->GetValue();
257 const wxString head = m_comboHead->GetValue();
258
259 // Non-recursive Mkdir keeps the creation atomic. wxPATH_MKDIR_FULL would
260 // silently accept an attacker-precreated leaf, reintroducing the TOCTOU
261 // hole the previous CreateTempFileName + remove + Mkdir pattern had.
262 const wxString tempBase =
263 wxStandardPaths::Get().GetTempDir() + wxFileName::GetPathSeparator()
264 + wxS( "kicad_diff_" )
265 + wxString::Format( wxS( "%lu_" ),
266 static_cast<unsigned long>( wxGetProcessId() ) );
267
268 const unsigned millis = static_cast<unsigned>( wxGetLocalTimeMillis().GetLo() );
269 wxString tmpRoot;
270
271 for( int attempt = 0; attempt < 16 && tmpRoot.IsEmpty(); ++attempt )
272 {
273 wxString candidate = tempBase + wxString::Format( wxS( "%d_%u" ), attempt, millis );
274
275 if( wxFileName::Mkdir( candidate, 0700, 0 ) )
276 tmpRoot = candidate;
277 }
278
279 if( tmpRoot.IsEmpty() )
280 {
281 wxMessageBox( _( "Could not create a temporary directory." ), _( "Compare File" ),
282 wxOK | wxICON_ERROR, this );
283 return;
284 }
285
286 TempDirGuard tempGuard{ tmpRoot };
287
288 const bool hasBase = file.status != KIGIT::FILE_CHANGE_STATUS::ADDED;
289 const bool hasHead = file.status != KIGIT::FILE_CHANGE_STATUS::REMOVED;
290 const wxString basePath = !file.oldPath.IsEmpty() ? file.oldPath : file.path;
291
292 wxString pathA;
293 wxString pathB;
294
295 if( prettyFootprint )
296 {
297 // Build minimal .pretty directories on each side so FP_LIB_DIFFER can
298 // operate. The footprint's name inside the library is the filename
299 // without extension; both sides keep the same name.
300 wxFileName srcFn( file.path );
301 const wxString fpFile = srcFn.GetFullName();
302
303 wxString dirA = tmpRoot + wxFileName::GetPathSeparator() + wxS( "base.pretty" );
304 wxString dirB = tmpRoot + wxFileName::GetPathSeparator() + wxS( "head.pretty" );
305
306 if( hasBase && !wxFileName::Mkdir( dirA, 0700, wxPATH_MKDIR_FULL ) )
307 {
308 return;
309 }
310
311 if( hasHead && !wxFileName::Mkdir( dirB, 0700, wxPATH_MKDIR_FULL ) )
312 {
313 return;
314 }
315
316 if( hasBase )
317 {
318 wxString blobPath = dirA + wxFileName::GetPathSeparator() + fpFile;
319
320 if( !writeRefBlobToFile( m_repo, base, basePath, blobPath ) )
321 {
322 wxMessageBox( wxString::Format( _( "Could not extract %s from %s." ),
323 basePath, base ),
324 _( "Compare File" ), wxOK | wxICON_ERROR, this );
325 return;
326 }
327
328 pathA = dirA;
329 }
330
331 if( hasHead )
332 {
333 wxString blobPath = dirB + wxFileName::GetPathSeparator() + fpFile;
334
335 if( !writeRefBlobToFile( m_repo, head, file.path, blobPath ) )
336 {
337 wxMessageBox( wxString::Format( _( "Could not extract %s from %s." ),
338 file.path, head ),
339 _( "Compare File" ), wxOK | wxICON_ERROR, this );
340 return;
341 }
342
343 pathB = dirB;
344 }
345 }
346 else
347 {
348 wxFileName srcName( file.path );
349
350 if( hasBase )
351 {
352 pathA = tmpRoot + wxFileName::GetPathSeparator() + wxS( "base_" )
353 + srcName.GetFullName();
354
355 if( !writeRefBlobToFile( m_repo, base, basePath, pathA ) )
356 {
357 wxMessageBox( wxString::Format( _( "Could not extract %s from %s." ),
358 basePath, base ),
359 _( "Compare File" ), wxOK | wxICON_ERROR, this );
360 return;
361 }
362 }
363
364 if( hasHead )
365 {
366 pathB = tmpRoot + wxFileName::GetPathSeparator() + wxS( "head_" )
367 + srcName.GetFullName();
368
369 if( !writeRefBlobToFile( m_repo, head, file.path, pathB ) )
370 {
371 wxMessageBox( wxString::Format( _( "Could not extract %s from %s." ),
372 file.path, head ),
373 _( "Compare File" ), wxOK | wxICON_ERROR, this );
374 return;
375 }
376 }
377 }
378
379 // pathA empty for ADDED / pathB empty for REMOVED — the kiface synthesizes
380 // an empty document for the missing side.
381 const wxString labelA = wxString::Format( wxS( "%s:%s" ), base, basePath );
382 const wxString labelB = wxString::Format( wxS( "%s:%s" ), head, file.path );
383
385 KICAD_DIFF::DispatchOpenDiffDialog( Kiway(), kind, pathA, pathB, labelA, labelB, this,
386 &reporter );
387
388 if( reporter.HasMessage() )
389 wxMessageBox( reporter.GetMessages(), _( "Compare File" ),
390 wxOK | wxICON_INFORMATION, this );
391}
DIALOG_GIT_MR_REVIEW_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxString &title=_("Compare Branches"), const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(800, 550), long style=wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
void OnFileActivated(wxListEvent &aEvent) override
DIALOG_GIT_MR_REVIEW(wxWindow *aParent, git_repository *aRepo, const std::vector< wxString > &aRefList)
void OnCompareClick(wxCommandEvent &aEvent) override
std::vector< KIGIT::CHANGED_FILE > m_changedFiles
void openFileDiff(long aListIndex)
void OnClose(wxCloseEvent &aEvent) override
KIWAY & Kiway() const
Return a reference to the KIWAY that this object has an opportunity to participate in.
A wrapper for reporting to a wxString object.
Definition reporter.h:189
#define _(s)
DOC_KIND DocKindFromExtension(const wxString &aPath)
Map a path's extension to a DOC_KIND (.kicad_pcb -> PCB, .kicad_sch -> SCH, .kicad_sym -> SYM_LIB,...
DOC_KIND
Document type a diff/merge entry point should route to, derived from a file path's extension.
int DispatchOpenDiffDialog(KIWAY &aKiway, DOC_KIND aKind, const wxString &aFileA, const wxString &aFileB, const wxString &aLabelA, const wxString &aLabelB, wxWindow *aParent, REPORTER *aReporter)
Open DIALOG_KICAD_DIFF on two files by calling the owning kiface's KIFACE_OPEN_DIFF_DIALOG function e...
std::unique_ptr< git_tree, decltype([](git_tree *aTree) { git_tree_free(aTree); })> GitTreePtr
A unique pointer for git_tree objects with automatic cleanup.
std::vector< CHANGED_FILE > CompareRefs(git_repository *aRepo, const wxString &aBaseRef, const wxString &aHeadRef)
Compare two git refs (branch / tag / commit OID) within a repository and return the per-file change l...
git_tree * ResolveRefToTree(git_repository *aRepo, const wxString &aRef)
Resolve a string ref (branch name, short OID, full OID, tag) to its tree.
const char * FileChangeStatusToString(FILE_CHANGE_STATUS aStatus)
std::unique_ptr< git_object, decltype([](git_object *aObject) { git_object_free(aObject); })> GitObjectPtr
A unique pointer for git_object objects with automatic cleanup.
FILE_CHANGE_STATUS status
IbisParser parser & reporter