KiCad PCB EDA Suite
Loading...
Searching...
No Matches
local_history.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
24#include <local_history.h>
25#include <history_lock.h>
26#include <lockfile.h>
28#include <pgm_base.h>
29#include <trace_helpers.h>
31#include <confirm.h>
32
33#include <git2.h>
34#include <wx/filename.h>
35#include <wx/filefn.h>
36#include <wx/ffile.h>
37#include <wx/dir.h>
38#include <wx/datetime.h>
39#include <wx/log.h>
40#include <wx/msgdlg.h>
41#include <wx/choicdlg.h>
42
43#include <vector>
44#include <string>
45#include <memory>
46#include <algorithm>
47#include <set>
48#include <map>
49#include <functional>
50#include <cstring>
51
52static wxString historyPath( const wxString& aProjectPath )
53{
54 wxFileName p( aProjectPath, wxEmptyString );
55 p.AppendDir( wxS( ".history" ) );
56 return p.GetPath();
57}
58
62
66
67void LOCAL_HISTORY::NoteFileChange( const wxString& aFile )
68{
69 wxFileName fn( aFile );
70
71 if( fn.GetFullName() == wxS( "fp-info-cache" ) || !Pgm().GetCommonSettings()->m_Backup.enabled )
72 return;
73
74 m_pendingFiles.insert( fn.GetFullPath() );
75}
76
77
78void LOCAL_HISTORY::RegisterSaver( const void* aSaverObject,
79 const std::function<void( const wxString&, std::vector<wxString>& )>& aSaver )
80{
81 if( m_savers.find( aSaverObject ) != m_savers.end() )
82 {
83 wxLogTrace( traceAutoSave, wxS("[history] Saver %p already registered, skipping"), aSaverObject );
84 return;
85 }
86
87 m_savers[aSaverObject] = aSaver;
88 wxLogTrace( traceAutoSave, wxS("[history] Registered saver %p (total=%zu)"), aSaverObject, m_savers.size() );
89}
90
91
92void LOCAL_HISTORY::UnregisterSaver( const void* aSaverObject )
93{
94 auto it = m_savers.find( aSaverObject );
95
96 if( it != m_savers.end() )
97 {
98 m_savers.erase( it );
99 wxLogTrace( traceAutoSave, wxS("[history] Unregistered saver %p (total=%zu)"), aSaverObject, m_savers.size() );
100 }
101}
102
103
105{
106 m_savers.clear();
107 wxLogTrace( traceAutoSave, wxS("[history] Cleared all savers") );
108}
109
110
111bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath, const wxString& aTitle )
112{
113 if( !Pgm().GetCommonSettings()->m_Backup.enabled )
114 {
115 wxLogTrace( traceAutoSave, wxS("Autosave disabled, returning" ) );
116 return true;
117 }
118
119 wxLogTrace( traceAutoSave, wxS("[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu"),
120 aProjectPath, aTitle, m_savers.size() );
121
122 if( m_savers.empty() )
123 {
124 wxLogTrace( traceAutoSave, wxS("[history] no savers registered; skipping") );
125 return false;
126 }
127
128 std::vector<wxString> files;
129
130 for( const auto& [saverObject, saver] : m_savers )
131 {
132 size_t before = files.size();
133 saver( aProjectPath, files );
134 wxLogTrace( traceAutoSave, wxS("[history] saver %p added %zu files (total=%zu)"),
135 saverObject, files.size() - before, files.size() );
136 }
137
138 // Filter out any files not within the project directory
139 wxString projectDir = aProjectPath;
140 if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
141 projectDir += wxFileName::GetPathSeparator();
142
143 auto it = std::remove_if( files.begin(), files.end(),
144 [&projectDir]( const wxString& file )
145 {
146 if( !file.StartsWith( projectDir ) )
147 {
148 wxLogTrace( traceAutoSave, wxS("[history] filtered out file outside project: %s"), file );
149 return true;
150 }
151 return false;
152 } );
153 files.erase( it, files.end() );
154
155 if( files.empty() )
156 {
157 wxLogTrace( traceAutoSave, wxS("[history] saver set produced no files; skipping") );
158 return false;
159 }
160
161 // Acquire locks using hybrid locking strategy
162 HISTORY_LOCK_MANAGER lock( aProjectPath );
163
164 if( !lock.IsLocked() )
165 {
166 wxLogTrace( traceAutoSave, wxS("[history] failed to acquire lock: %s"), lock.GetLockError() );
167 return false;
168 }
169
170 git_repository* repo = lock.GetRepository();
171 git_index* index = lock.GetIndex();
172 wxString hist = historyPath( aProjectPath );
173
174 // Stage selected files (mirroring logic from CommitSnapshot but limited to given files)
175 for( const wxString& file : files )
176 {
177 wxFileName src( file );
178
179 if( !src.FileExists() )
180 {
181 wxLogTrace( traceAutoSave, wxS("[history] skip missing '%s'"), file );
182 continue;
183 }
184
185 // If saver already produced a path inside history mirror, just stage it.
186 if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
187 {
188 std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
189 git_index_add_bypath( index, relHist.c_str() );
190 wxLogTrace( traceAutoSave, wxS("[history] staged pre-mirrored '%s'"), file );
191 continue;
192 }
193
194 wxString relStr;
195 wxString proj = wxFileName( aProjectPath ).GetFullPath();
196
197 if( src.GetFullPath().StartsWith( proj + wxFILE_SEP_PATH ) )
198 relStr = src.GetFullPath().Mid( proj.length() + 1 );
199 else
200 relStr = src.GetFullName();
201
202 wxFileName dst( hist + wxFILE_SEP_PATH + relStr );
203 wxFileName dstDir( dst );
204 dstDir.SetFullName( wxEmptyString );
205 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
206 wxCopyFile( src.GetFullPath(), dst.GetFullPath(), true );
207 std::string rel = dst.GetFullPath().ToStdString().substr( hist.length() + 1 );
208 git_index_add_bypath( index, rel.c_str() );
209 wxLogTrace( traceAutoSave, wxS("[history] staged '%s' as '%s'"), file, wxString::FromUTF8( rel ) );
210 }
211
212 // Compare index to HEAD; if no diff -> abort to avoid empty commit.
213 git_oid head_oid;
214 git_commit* head_commit = nullptr;
215 git_tree* head_tree = nullptr;
216
217 bool headExists = ( git_reference_name_to_id( &head_oid, repo, "HEAD" ) == 0 )
218 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
219 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
220
221 git_tree* rawIndexTree = nullptr;
222 git_oid index_tree_oid;
223
224 if( git_index_write_tree( &index_tree_oid, index ) != 0 )
225 {
226 if( head_tree ) git_tree_free( head_tree );
227 if( head_commit ) git_commit_free( head_commit );
228 wxLogTrace( traceAutoSave, wxS("[history] failed to write index tree" ) );
229 return false;
230 }
231
232 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
233 std::unique_ptr<git_tree, decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
234
235 bool hasChanges = true;
236
237 if( headExists )
238 {
239 git_diff* diff = nullptr;
240
241 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(), nullptr ) == 0 )
242 {
243 hasChanges = git_diff_num_deltas( diff ) > 0;
244 wxLogTrace( traceAutoSave, wxS("[history] diff deltas=%u"), (unsigned) git_diff_num_deltas( diff ) );
245 git_diff_free( diff );
246 }
247 }
248
249 if( head_tree ) git_tree_free( head_tree );
250 if( head_commit ) git_commit_free( head_commit );
251
252 if( !hasChanges )
253 {
254 wxLogTrace( traceAutoSave, wxS("[history] no changes detected; no commit") );
255 return false; // Nothing new; skip commit.
256 }
257
258 git_signature* rawSig = nullptr;
259 git_signature_now( &rawSig, "KiCad", "[email protected]" );
260 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
261
262 git_commit* parent = nullptr;
263 git_oid parent_id;
264 int parents = 0;
265
266 if( git_reference_name_to_id( &parent_id, repo, "HEAD" ) == 0 )
267 {
268 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
269 parents = 1;
270 }
271
272 wxString msg = aTitle.IsEmpty() ? wxString( "Autosave" ) : aTitle;
273 git_oid commit_id;
274 const git_commit* constParent = parent;
275
276 int rc = git_commit_create( &commit_id, repo, "HEAD", sig.get(), sig.get(), nullptr,
277 msg.mb_str().data(), indexTree.get(), parents,
278 parents ? &constParent : nullptr );
279
280 if( rc == 0 )
281 wxLogTrace( traceAutoSave, wxS("[history] commit created %s (%s files=%zu)"),
282 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, files.size() );
283 else
284 wxLogTrace( traceAutoSave, wxS("[history] commit failed rc=%d"), rc );
285
286 if( parent ) git_commit_free( parent );
287
288 git_index_write( index );
289 return rc == 0;
290}
291
292
294{
295 std::vector<wxString> files( m_pendingFiles.begin(), m_pendingFiles.end() );
296 m_pendingFiles.clear();
297 return CommitSnapshot( files, wxS( "Autosave" ) );
298}
299
300
301bool LOCAL_HISTORY::Init( const wxString& aProjectPath )
302{
303 if( aProjectPath.IsEmpty() )
304 return false;
305
306 if( !Pgm().GetCommonSettings()->m_Backup.enabled )
307 return true;
308
309 wxString hist = historyPath( aProjectPath );
310
311 if( !wxDirExists( hist ) )
312 {
313 if( wxIsWritable( aProjectPath ) )
314 {
315 if( !wxMkdir( hist ) )
316 {
317 return false;
318 }
319 }
320 }
321
322 git_repository* rawRepo = nullptr;
323 bool isNewRepo = false;
324
325 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
326 {
327 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
328 return false;
329
330 isNewRepo = true;
331
332 wxFileName ignoreFile( hist, wxS( ".gitignore" ) );
333 if( !ignoreFile.FileExists() )
334 {
335 wxFFile f( ignoreFile.GetFullPath(), wxT( "w" ) );
336 if( f.IsOpened() )
337 {
338 f.Write( wxS( "fp-info-cache\n*-backups\nREADME.txt\n" ) );
339 f.Close();
340 }
341 }
342
343 wxFileName readmeFile( hist, wxS( "README.txt" ) );
344
345 if( !readmeFile.FileExists() )
346 {
347 wxFFile f( readmeFile.GetFullPath(), wxT( "w" ) );
348
349 if( f.IsOpened() )
350 {
351 f.Write( wxS( "KiCad Local History Directory\n"
352 "=============================\n\n"
353 "This directory contains automatic snapshots of your project files.\n"
354 "KiCad periodically saves copies of your work here, allowing you to\n"
355 "recover from accidental changes or data loss.\n\n"
356 "You can browse and restore previous versions through KiCad's\n"
357 "File > Local History menu.\n\n"
358 "To disable this feature:\n"
359 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
360 "This directory can be safely deleted if you no longer need the\n"
361 "history, but doing so will permanently remove all saved snapshots.\n" ) );
362 f.Close();
363 }
364 }
365 }
366
367 git_repository_free( rawRepo );
368
369 // If this is a newly initialized repository with no commits, create an initial snapshot
370 // of all existing project files
371 if( isNewRepo )
372 {
373 wxLogTrace( traceAutoSave, wxS( "[history] Init: New repository created, collecting existing files" ) );
374
375 // Collect all files in the project directory (excluding backups and hidden files)
376 wxArrayString files;
377 std::function<void( const wxString& )> collect = [&]( const wxString& path )
378 {
379 wxString name;
380 wxDir d( path );
381
382 if( !d.IsOpened() )
383 return;
384
385 bool cont = d.GetFirst( &name );
386
387 while( cont )
388 {
389 // Skip hidden files/directories and backup directories
390 if( name.StartsWith( wxS( "." ) ) || name.EndsWith( wxS( "-backups" ) ) )
391 {
392 cont = d.GetNext( &name );
393 continue;
394 }
395
396 wxFileName fn( path, name );
397 wxString fullPath = fn.GetFullPath();
398
399 if( wxFileName::DirExists( fullPath ) )
400 {
401 collect( fullPath );
402 }
403 else if( fn.FileExists() )
404 {
405 // Skip transient files
406 if( fn.GetFullName() != wxS( "fp-info-cache" ) )
407 files.Add( fn.GetFullPath() );
408 }
409
410 cont = d.GetNext( &name );
411 }
412 };
413
414 collect( aProjectPath );
415
416 if( files.GetCount() > 0 )
417 {
418 std::vector<wxString> vec;
419 vec.reserve( files.GetCount() );
420
421 for( unsigned i = 0; i < files.GetCount(); ++i )
422 vec.push_back( files[i] );
423
424 wxLogTrace( traceAutoSave, wxS( "[history] Init: Creating initial snapshot with %zu files" ), vec.size() );
425 CommitSnapshot( vec, wxS( "Initial snapshot" ) );
426
427 // Tag the initial snapshot as saved so HeadNewerThanLastSave() doesn't
428 // incorrectly offer to restore when the project is first opened
429 TagSave( aProjectPath, wxS( "project" ) );
430 }
431 else
432 {
433 wxLogTrace( traceAutoSave, wxS( "[history] Init: No files found to add to initial snapshot" ) );
434 }
435 }
436
437 return true;
438}
439
440
441// Helper function to commit files using an already-acquired lock
442static bool commitSnapshotWithLock( git_repository* repo, git_index* index,
443 const wxString& aHistoryPath, const wxString& aProjectPath,
444 const std::vector<wxString>& aFiles, const wxString& aTitle )
445{
446 std::vector<std::string> filesArrStr;
447
448 for( const wxString& file : aFiles )
449 {
450 wxFileName src( file );
451 wxString relPath;
452
453 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
454 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
455 else
456 relPath = src.GetFullName(); // Fallback (should not normally happen)
457
458 relPath.Replace( "\\", "/" ); // libgit2 needs forward slashes on all platforms
459 std::string relPathStr = relPath.ToStdString();
460
461 unsigned int status = 0;
462 int rc = git_status_file( &status, repo, relPathStr.data() );
463
464 if( rc == 0 && status != 0 )
465 {
466 wxLogTrace( traceAutoSave, wxS( "File %s status %d " ), relPath, status );
467 filesArrStr.emplace_back( relPathStr );
468 }
469 else if( rc != 0 )
470 {
471 wxLogTrace( traceAutoSave, wxS( "File %s status error %d " ), relPath, rc );
472 filesArrStr.emplace_back( relPathStr ); // Add anyway even if the file is untracked.
473 }
474 }
475
476 std::vector<char*> cStrings( filesArrStr.size() );
477
478 for( size_t i = 0; i < filesArrStr.size(); i++ )
479 cStrings[i] = filesArrStr[i].data();
480
481 git_strarray filesArrGit;
482 filesArrGit.count = filesArrStr.size();
483 filesArrGit.strings = cStrings.data();
484
485 if( filesArrStr.size() == 0 )
486 {
487 wxLogTrace( traceAutoSave, wxS( "No changes, skipping" ) );
488 return false;
489 }
490
491 int rc = git_index_add_all( index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
492 NULL );
493 wxLogTrace( traceAutoSave, wxS( "Adding %zu files, rc %d" ), filesArrStr.size(), rc );
494
495 if( rc != 0 )
496 return false;
497
498 git_oid tree_id;
499 if( git_index_write_tree( &tree_id, index ) != 0 )
500 return false;
501
502 git_tree* rawTree = nullptr;
503 git_tree_lookup( &rawTree, repo, &tree_id );
504 std::unique_ptr<git_tree, decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
505
506 git_signature* rawSig = nullptr;
507 git_signature_now( &rawSig, "KiCad", "[email protected]" );
508 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( rawSig,
509 &git_signature_free );
510
511 git_commit* rawParent = nullptr;
512 git_oid parent_id;
513 int parents = 0;
514
515 if( git_reference_name_to_id( &parent_id, repo, "HEAD" ) == 0 )
516 {
517 git_commit_lookup( &rawParent, repo, &parent_id );
518 parents = 1;
519 }
520
521 std::unique_ptr<git_commit, decltype( &git_commit_free )> parent( rawParent,
522 &git_commit_free );
523
524 git_tree* rawParentTree = nullptr;
525
526 if( parent )
527 git_commit_tree( &rawParentTree, parent.get() );
528
529 std::unique_ptr<git_tree, decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
530
531 git_diff* rawDiff = nullptr;
532 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(), index, nullptr );
533 std::unique_ptr<git_diff, decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
534
535 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
536
537 if( numChangedFiles == 0 )
538 {
539 wxLogTrace( traceAutoSave, wxS( "No actual changes in tree, skipping commit" ) );
540 return false;
541 }
542
543 wxString msg;
544
545 if( !aTitle.IsEmpty() )
546 msg << aTitle << wxS( ": " );
547
548 msg << numChangedFiles << wxS( " files changed" );
549
550 for( size_t i = 0; i < numChangedFiles; ++i )
551 {
552 const git_diff_delta* delta = git_diff_get_delta( diff.get(), i );
553 git_patch* rawPatch = nullptr;
554 git_patch_from_diff( &rawPatch, diff.get(), i );
555 std::unique_ptr<git_patch, decltype( &git_patch_free )> patch( rawPatch,
556 &git_patch_free );
557 size_t context = 0, adds = 0, dels = 0;
558 git_patch_line_stats( &context, &adds, &dels, patch.get() );
559 size_t updated = std::min( adds, dels );
560 adds -= updated;
561 dels -= updated;
562 msg << wxS( "\n" ) << wxString::FromUTF8( delta->new_file.path )
563 << wxS( " " ) << adds << wxS( "/" ) << dels << wxS( "/" ) << updated;
564 }
565
566 git_oid commit_id;
567 git_commit* parentPtr = parent.get();
568 const git_commit* constParentPtr = parentPtr;
569 git_commit_create( &commit_id, repo, "HEAD", sig.get(), sig.get(), nullptr,
570 msg.mb_str().data(), tree.get(), parents,
571 parentPtr ? &constParentPtr : nullptr );
572 git_index_write( index );
573 return true;
574}
575
576
577bool LOCAL_HISTORY::CommitSnapshot( const std::vector<wxString>& aFiles, const wxString& aTitle )
578{
579 if( aFiles.empty() || !Pgm().GetCommonSettings()->m_Backup.enabled )
580 return true;
581
582 wxString proj = wxFileName( aFiles[0] ).GetPath();
583 wxString hist = historyPath( proj );
584
585 // Acquire locks using hybrid locking strategy
586 HISTORY_LOCK_MANAGER lock( proj );
587
588 if( !lock.IsLocked() )
589 {
590 wxLogTrace( traceAutoSave, wxS("[history] CommitSnapshot failed to acquire lock: %s"),
591 lock.GetLockError() );
592 return false;
593 }
594
595 git_repository* repo = lock.GetRepository();
596 git_index* index = lock.GetIndex();
597
598 return commitSnapshotWithLock( repo, index, hist, proj, aFiles, aTitle );
599}
600
601
602// Helper to collect all project files (excluding .history, backups, and transient files)
603static void collectProjectFiles( const wxString& aProjectPath, std::vector<wxString>& aFiles )
604{
605 wxDir dir( aProjectPath );
606
607 if( !dir.IsOpened() )
608 return;
609
610 // Collect recursively.
611 std::function<void(const wxString&)> collect = [&]( const wxString& path )
612 {
613 wxString name;
614 wxDir d( path );
615
616 if( !d.IsOpened() )
617 return;
618
619 bool cont = d.GetFirst( &name );
620
621 while( cont )
622 {
623 if( name == wxS( ".history" ) || name.EndsWith( wxS( "-backups" ) ) )
624 {
625 cont = d.GetNext( &name );
626 continue; // Skip history repo itself.
627 }
628
629 wxFileName fn( path, name );
630 wxString fullPath = fn.GetFullPath();
631
632 if( wxFileName::DirExists( fullPath ) )
633 {
634 collect( fullPath );
635 }
636 else if( fn.FileExists() )
637 {
638 // Reuse NoteFileChange filters implicitly by skipping the transient names.
639 if( fn.GetFullName() != wxS( "fp-info-cache" ) )
640 aFiles.push_back( fn.GetFullPath() );
641 }
642
643 cont = d.GetNext( &name );
644 }
645 };
646
647 collect( aProjectPath );
648}
649
650
651bool LOCAL_HISTORY::CommitFullProjectSnapshot( const wxString& aProjectPath, const wxString& aTitle )
652{
653 std::vector<wxString> files;
654 collectProjectFiles( aProjectPath, files );
655
656 if( files.empty() )
657 return false;
658
659 return CommitSnapshot( files, aTitle );
660}
661
662bool LOCAL_HISTORY::HistoryExists( const wxString& aProjectPath )
663{
664 return wxDirExists( historyPath( aProjectPath ) );
665}
666
667bool LOCAL_HISTORY::TagSave( const wxString& aProjectPath, const wxString& aFileType )
668{
669 HISTORY_LOCK_MANAGER lock( aProjectPath );
670
671 if( !lock.IsLocked() )
672 {
673 wxLogTrace( traceAutoSave, wxS( "[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
674 return false;
675 }
676
677 git_repository* repo = lock.GetRepository();
678
679 if( !repo )
680 return false;
681
682 git_oid head;
683 if( git_reference_name_to_id( &head, repo, "HEAD" ) != 0 )
684 return false;
685
686 wxString tagName;
687 int i = 1;
688 git_reference* ref = nullptr;
689 do
690 {
691 tagName.Printf( wxS( "Save_%s_%d" ), aFileType, i++ );
692 } while( git_reference_lookup( &ref, repo, ( wxS( "refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
693
694 git_oid tag_oid;
695 git_object* head_obj = nullptr;
696 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
697 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
698 git_object_free( head_obj );
699
700 wxString lastName;
701 lastName.Printf( wxS( "Last_Save_%s" ), aFileType );
702 if( git_reference_lookup( &ref, repo, ( wxS( "refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
703 {
704 git_reference_delete( ref );
705 git_reference_free( ref );
706 }
707
708 git_oid last_tag_oid;
709 git_object* head_obj2 = nullptr;
710 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
711 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
712 git_object_free( head_obj2 );
713
714 return true;
715}
716
717bool LOCAL_HISTORY::HeadNewerThanLastSave( const wxString& aProjectPath )
718{
719 wxString hist = historyPath( aProjectPath );
720 git_repository* repo = nullptr;
721
722 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
723 return false;
724
725 git_oid head_oid;
726 if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
727 {
728 git_repository_free( repo );
729 return false;
730 }
731
732 git_commit* head_commit = nullptr;
733 git_commit_lookup( &head_commit, repo, &head_oid );
734 git_time_t head_time = git_commit_time( head_commit );
735
736 git_strarray tags;
737 git_tag_list_match( &tags, "Last_Save_*", repo );
738 git_time_t save_time = 0;
739
740 for( size_t i = 0; i < tags.count; ++i )
741 {
742 git_reference* ref = nullptr;
743 if( git_reference_lookup( &ref, repo,
744 ( wxS( "refs/tags/" ) +
745 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
746 {
747 const git_oid* oid = git_reference_target( ref );
748 git_commit* c = nullptr;
749 if( git_commit_lookup( &c, repo, oid ) == 0 )
750 {
751 git_time_t t = git_commit_time( c );
752 if( t > save_time )
753 save_time = t;
754 git_commit_free( c );
755 }
756 git_reference_free( ref );
757 }
758 }
759
760 git_strarray_free( &tags );
761 git_commit_free( head_commit );
762 git_repository_free( repo );
763
764 // If there are no Last_Save tags but there IS a HEAD commit, we have autosaved
765 // data that was never explicitly saved - offer to restore
766 if( save_time == 0 )
767 return true;
768
769 return head_time > save_time;
770}
771
772bool LOCAL_HISTORY::CommitDuplicateOfLastSave( const wxString& aProjectPath, const wxString& aFileType,
773 const wxString& aMessage )
774{
775 HISTORY_LOCK_MANAGER lock( aProjectPath );
776
777 if( !lock.IsLocked() )
778 {
779 wxLogTrace( traceAutoSave, wxS( "[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
780 return false;
781 }
782
783 git_repository* repo = lock.GetRepository();
784
785 if( !repo )
786 return false;
787
788 wxString lastName; lastName.Printf( wxS("Last_Save_%s"), aFileType );
789 git_reference* lastRef = nullptr;
790 if( git_reference_lookup( &lastRef, repo, ( wxS("refs/tags/") + lastName ).mb_str().data() ) != 0 )
791 return false; // no tag to duplicate
792 std::unique_ptr<git_reference, decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
793
794 const git_oid* lastOid = git_reference_target( lastRef );
795 git_commit* lastCommit = nullptr;
796 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
797 return false;
798 std::unique_ptr<git_commit, decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
799
800 git_tree* lastTree = nullptr;
801 git_commit_tree( &lastTree, lastCommit );
802 std::unique_ptr<git_tree, decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
803
804 // Parent will be current HEAD (to keep linear history)
805 git_oid headOid;
806 git_commit* headCommit = nullptr;
807 int parents = 0;
808 const git_commit* parentArray[1];
809 if( git_reference_name_to_id( &headOid, repo, "HEAD" ) == 0 &&
810 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
811 {
812 parentArray[0] = headCommit;
813 parents = 1;
814 }
815
816 git_signature* sigRaw = nullptr;
817 git_signature_now( &sigRaw, "KiCad", "[email protected]" );
818 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
819
820 wxString msg = aMessage.IsEmpty() ? wxS("Discard unsaved ") + aFileType : aMessage;
821 git_oid newCommitOid;
822 int rc = git_commit_create( &newCommitOid, repo, "HEAD", sig.get(), sig.get(), nullptr,
823 msg.mb_str().data(), lastTree, parents, parents ? parentArray : nullptr );
824 if( headCommit ) git_commit_free( headCommit );
825 if( rc != 0 )
826 return false;
827
828 // Move Last_Save tag to new commit
829 git_reference* existing = nullptr;
830 if( git_reference_lookup( &existing, repo, ( wxS("refs/tags/") + lastName ).mb_str().data() ) == 0 )
831 {
832 git_reference_delete( existing );
833 git_reference_free( existing );
834 }
835 git_object* newCommitObj = nullptr;
836 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
837 {
838 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
839 git_object_free( newCommitObj );
840 }
841 return true;
842}
843
844static size_t dirSizeRecursive( const wxString& path )
845{
846 size_t total = 0;
847 wxDir dir( path );
848 if( !dir.IsOpened() )
849 return 0;
850 wxString name;
851 bool cont = dir.GetFirst( &name );
852 while( cont )
853 {
854 wxFileName fn( path, name );
855 wxString fullPath = fn.GetFullPath();
856
857 if( wxFileName::DirExists( fullPath ) )
858 total += dirSizeRecursive( fullPath );
859 else if( fn.FileExists() )
860 total += (size_t) fn.GetSize().GetValue();
861 cont = dir.GetNext( &name );
862 }
863 return total;
864}
865
866bool LOCAL_HISTORY::EnforceSizeLimit( const wxString& aProjectPath, size_t aMaxBytes )
867{
868 if( aMaxBytes == 0 )
869 return false;
870
871 wxString hist = historyPath( aProjectPath );
872
873 if( !wxDirExists( hist ) )
874 return false;
875
876 size_t current = dirSizeRecursive( hist );
877
878 if( current <= aMaxBytes )
879 return true; // within limit
880
881 HISTORY_LOCK_MANAGER lock( aProjectPath );
882
883 if( !lock.IsLocked() )
884 {
885 wxLogTrace( traceAutoSave, wxS( "[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
886 return false;
887 }
888
889 git_repository* repo = lock.GetRepository();
890
891 if( !repo )
892 return false;
893
894 // Collect commits newest-first using revwalk
895 git_revwalk* walk = nullptr;
896 git_revwalk_new( &walk, repo );
897 git_revwalk_sorting( walk, GIT_SORT_TIME );
898 git_revwalk_push_head( walk );
899 std::vector<git_oid> commits;
900 git_oid oid;
901
902 while( git_revwalk_next( &oid, walk ) == 0 )
903 commits.push_back( oid );
904
905 git_revwalk_free( walk );
906
907 if( commits.empty() )
908 return true;
909
910 // Determine set of newest commits to keep based on blob sizes.
911 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )> seenBlobs(
912 []( const git_oid& a, const git_oid& b )
913 {
914 return memcmp( &a, &b, sizeof( git_oid ) ) < 0;
915 } );
916
917 size_t keptBytes = 0;
918 std::vector<git_oid> keep;
919
920 git_odb* odb = nullptr;
921 git_repository_odb( &odb, repo );
922
923 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
924 {
925 size_t added = 0;
926 size_t cnt = git_tree_entrycount( tree );
927
928 for( size_t i = 0; i < cnt; ++i )
929 {
930 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
931
932 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
933 {
934 const git_oid* bid = git_tree_entry_id( entry );
935
936 if( seenBlobs.find( *bid ) == seenBlobs.end() )
937 {
938 size_t len = 0;
939 git_object_t type = GIT_OBJECT_ANY;
940
941 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
942 added += len;
943
944 seenBlobs.insert( *bid );
945 }
946 }
947 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
948 {
949 git_tree* sub = nullptr;
950
951 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
952 {
953 added += accountTree( sub );
954 git_tree_free( sub );
955 }
956 }
957 }
958
959 return added;
960 };
961
962 for( const git_oid& cOid : commits )
963 {
964 git_commit* c = nullptr;
965
966 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
967 continue;
968
969 git_tree* tree = nullptr;
970 git_commit_tree( &tree, c );
971 size_t add = accountTree( tree );
972 git_tree_free( tree );
973 git_commit_free( c );
974
975 if( keep.empty() || keptBytes + add <= aMaxBytes )
976 {
977 keep.push_back( cOid );
978 keptBytes += add;
979 }
980 else
981 break; // stop once limit exceeded
982 }
983
984 git_odb_free( odb );
985
986 if( keep.empty() )
987 keep.push_back( commits.front() ); // ensure at least head
988
989 // Collect tags we want to preserve (Save_*/Last_Save_*). We'll recreate them if their
990 // target commit is retained. Also ensure tagged commits are ALWAYS kept.
991 std::vector<std::pair<wxString, git_oid>> tagTargets;
992 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )> taggedCommits(
993 []( const git_oid& a, const git_oid& b )
994 {
995 return memcmp( &a, &b, sizeof( git_oid ) ) < 0;
996 } );
997 git_strarray tagList;
998
999 if( git_tag_list( &tagList, repo ) == 0 )
1000 {
1001 for( size_t i = 0; i < tagList.count; ++i )
1002 {
1003 wxString name = wxString::FromUTF8( tagList.strings[i] );
1004 if( name.StartsWith( wxS("Save_") ) || name.StartsWith( wxS("Last_Save_") ) )
1005 {
1006 git_reference* tref = nullptr;
1007
1008 if( git_reference_lookup( &tref, repo, ( wxS( "refs/tags/" ) + name ).mb_str().data() ) == 0 )
1009 {
1010 const git_oid* toid = git_reference_target( tref );
1011
1012 if( toid )
1013 {
1014 tagTargets.emplace_back( name, *toid );
1015 taggedCommits.insert( *toid );
1016
1017 // Ensure this tagged commit is in the keep list
1018 bool found = false;
1019 for( const auto& k : keep )
1020 {
1021 if( memcmp( &k, toid, sizeof( git_oid ) ) == 0 )
1022 {
1023 found = true;
1024 break;
1025 }
1026 }
1027
1028 if( !found )
1029 {
1030 // Add tagged commit to keep list (even if it exceeds size limit)
1031 keep.push_back( *toid );
1032 wxLogTrace( traceAutoSave, wxS( "[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1033 name );
1034 }
1035 }
1036
1037 git_reference_free( tref );
1038 }
1039 }
1040 }
1041 git_strarray_free( &tagList );
1042 }
1043
1044 // Rebuild trimmed repo in temp dir
1045 wxFileName trimFn( hist + wxS("_trim"), wxEmptyString );
1046 wxString trimPath = trimFn.GetPath();
1047
1048 if( wxDirExists( trimPath ) )
1049 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1050
1051 wxMkdir( trimPath );
1052 git_repository* newRepo = nullptr;
1053
1054 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1055 return false;
1056
1057 // We will materialize each kept commit chronologically (oldest first) to preserve order.
1058 std::reverse( keep.begin(), keep.end() );
1059 git_commit* parent = nullptr;
1060 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1061 std::vector<MAP_ENTRY> commitMap;
1062
1063 for( const git_oid& co : keep )
1064 {
1065 git_commit* orig = nullptr;
1066
1067 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1068 continue;
1069
1070 git_tree* tree = nullptr;
1071 git_commit_tree( &tree, orig );
1072
1073 // Checkout tree into filesystem (simple extractor)
1074 // Remove all files first (except .git).
1075 wxArrayString toDelete;
1076 wxDir d( trimPath );
1077 wxString nm;
1078 bool cont = d.GetFirst( &nm );
1079 while( cont )
1080 {
1081 if( nm != wxS(".git") )
1082 toDelete.Add( nm );
1083 cont = d.GetNext( &nm );
1084 }
1085
1086 for( auto& del : toDelete )
1087 {
1088 wxFileName f( trimPath, del );
1089 if( f.DirExists() )
1090 wxFileName::Rmdir( f.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1091 else if( f.FileExists() )
1092 wxRemoveFile( f.GetFullPath() );
1093 }
1094
1095 // Recursively write tree entries
1096 std::function<void(git_tree*, const wxString&)> writeTree = [&]( git_tree* t, const wxString& base )
1097 {
1098 size_t ecnt = git_tree_entrycount( t );
1099 for( size_t i = 0; i < ecnt; ++i )
1100 {
1101 const git_tree_entry* e = git_tree_entry_byindex( t, i );
1102 wxString name = wxString::FromUTF8( git_tree_entry_name( e ) );
1103
1104 if( git_tree_entry_type( e ) == GIT_OBJECT_TREE )
1105 {
1106 wxFileName dir( base, name );
1107 wxMkdir( dir.GetFullPath() );
1108 git_tree* sub = nullptr;
1109
1110 if( git_tree_lookup( &sub, repo, git_tree_entry_id( e ) ) == 0 )
1111 {
1112 writeTree( sub, dir.GetFullPath() );
1113 git_tree_free( sub );
1114 }
1115 }
1116 else if( git_tree_entry_type( e ) == GIT_OBJECT_BLOB )
1117 {
1118 git_blob* blob = nullptr;
1119
1120 if( git_blob_lookup( &blob, repo, git_tree_entry_id( e ) ) == 0 )
1121 {
1122 wxFileName file( base, name );
1123 wxFFile f( file.GetFullPath(), wxT("wb") );
1124
1125 if( f.IsOpened() )
1126 {
1127 f.Write( (const char*) git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1128 f.Close();
1129 }
1130 git_blob_free( blob );
1131 }
1132 }
1133 }
1134 };
1135
1136 writeTree( tree, trimPath );
1137
1138 git_index* newIndex = nullptr;
1139 git_repository_index( &newIndex, newRepo );
1140 git_index_add_all( newIndex, nullptr, 0, nullptr, nullptr );
1141 git_index_write( newIndex );
1142 git_oid newTreeOid;
1143 git_index_write_tree( &newTreeOid, newIndex );
1144 git_tree* newTree = nullptr;
1145 git_tree_lookup( &newTree, newRepo, &newTreeOid );
1146
1147 // Recreate original author/committer signatures preserving timestamp.
1148 const git_signature* origAuthor = git_commit_author( orig );
1149 const git_signature* origCommitter = git_commit_committer( orig );
1150 git_signature* sigAuthor = nullptr;
1151 git_signature* sigCommitter = nullptr;
1152
1153 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1154 origAuthor->when.time, origAuthor->when.offset );
1155 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1156 origCommitter->when.time, origCommitter->when.offset );
1157
1158 const git_commit* parents[1];
1159 int parentCount = 0;
1160
1161 if( parent )
1162 {
1163 parents[0] = parent;
1164 parentCount = 1;
1165 }
1166
1167 git_oid newCommitOid;
1168 git_commit_create( &newCommitOid, newRepo, "HEAD", sigAuthor, sigCommitter, nullptr, git_commit_message( orig ),
1169 newTree, parentCount, parentCount ? parents : nullptr );
1170 if( parent )
1171 git_commit_free( parent );
1172
1173 git_commit_lookup( &parent, newRepo, &newCommitOid );
1174
1175 commitMap.emplace_back( co, newCommitOid );
1176
1177 git_signature_free( sigAuthor );
1178 git_signature_free( sigCommitter );
1179 git_tree_free( newTree );
1180 git_index_free( newIndex );
1181 git_tree_free( tree );
1182 git_commit_free( orig );
1183 }
1184
1185 if( parent )
1186 git_commit_free( parent );
1187
1188 // Recreate preserved tags pointing to new commit OIDs where possible.
1189 for( const auto& tt : tagTargets )
1190 {
1191 // Find mapping
1192 const git_oid* newOid = nullptr;
1193
1194 for( const auto& m : commitMap )
1195 {
1196 if( memcmp( &m.orig, &tt.second, sizeof( git_oid ) ) == 0 )
1197 {
1198 newOid = &m.neu;
1199 break;
1200 }
1201 }
1202
1203 if( !newOid )
1204 continue; // commit trimmed away
1205
1206 git_object* obj = nullptr;
1207
1208 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1209 {
1210 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1211 git_object_free( obj );
1212 }
1213 }
1214
1215 // Close both repositories before swapping directories to avoid file locking issues.
1216 // Note: The lock manager will automatically free the original repo when it goes out of scope,
1217 // but we need to manually free the new trimmed repo we created.
1218 git_repository_free( newRepo );
1219
1220 // Replace old history dir with trimmed one
1221 wxString backupOld = hist + wxS("_old");
1222 wxRenameFile( hist, backupOld );
1223 wxRenameFile( trimPath, hist );
1224 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1225 return true;
1226}
1227
1228wxString LOCAL_HISTORY::GetHeadHash( const wxString& aProjectPath )
1229{
1230 wxString hist = historyPath( aProjectPath );
1231 git_repository* repo = nullptr;
1232
1233 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1234 return wxEmptyString;
1235
1236 git_oid head_oid;
1237 if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
1238 {
1239 git_repository_free( repo );
1240 return wxEmptyString;
1241 }
1242
1243 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1244 git_repository_free( repo );
1245 return hash;
1246}
1247
1248
1249// Helper functions for RestoreCommit
1250namespace
1251{
1252
1256bool checkForLockedFiles( const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1257{
1258 std::function<void( const wxString& )> findLocks = [&]( const wxString& dirPath )
1259 {
1260 wxDir dir( dirPath );
1261 if( !dir.IsOpened() )
1262 return;
1263
1264 wxString filename;
1265 bool cont = dir.GetFirst( &filename );
1266
1267 while( cont )
1268 {
1269 wxFileName fullPath( dirPath, filename );
1270
1271 // Skip special directories
1272 if( filename == wxS(".history") || filename == wxS(".git") )
1273 {
1274 cont = dir.GetNext( &filename );
1275 continue;
1276 }
1277
1278 if( fullPath.DirExists() )
1279 {
1280 findLocks( fullPath.GetFullPath() );
1281 }
1282 else if( fullPath.FileExists()
1283 && filename.StartsWith( FILEEXT::LockFilePrefix )
1284 && filename.EndsWith( wxString( wxS( "." ) ) + FILEEXT::LockFileExtension ) )
1285 {
1286 // Reconstruct the original filename from the lock file name
1287 // Lock files are: ~<original>.<ext>.lck -> need to get <original>.<ext>
1288 wxString baseName = filename.Mid( FILEEXT::LockFilePrefix.length() );
1289 baseName = baseName.BeforeLast( '.' ); // Remove .lck
1290 wxFileName originalFile( dirPath, baseName );
1291
1292 // Check if this is a valid LOCKFILE (not stale and not ours)
1293 LOCKFILE testLock( originalFile.GetFullPath() );
1294 if( testLock.Valid() && !testLock.IsLockedByMe() )
1295 {
1296 aLockedFiles.push_back( fullPath.GetFullPath() );
1297 }
1298 }
1299
1300 cont = dir.GetNext( &filename );
1301 }
1302 };
1303
1304 findLocks( aProjectPath );
1305 return aLockedFiles.empty();
1306}
1307
1308
1312bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree, const wxString& aTempPath )
1313{
1314 bool extractSuccess = true;
1315
1316 std::function<void( git_tree*, const wxString& )> extractTree =
1317 [&]( git_tree* t, const wxString& prefix )
1318 {
1319 if( !extractSuccess )
1320 return;
1321
1322 size_t cnt = git_tree_entrycount( t );
1323 for( size_t i = 0; i < cnt; ++i )
1324 {
1325 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1326 wxString name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1327 wxString fullPath = prefix.IsEmpty() ? name : prefix + wxS("/") + name;
1328
1329 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1330 {
1331 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
1332 wxEmptyString );
1333 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1334 {
1335 wxLogTrace( traceAutoSave,
1336 wxS( "[history] extractCommitToTemp: Failed to create directory '%s'" ),
1337 dirPath.GetPath() );
1338 extractSuccess = false;
1339 return;
1340 }
1341
1342 git_tree* sub = nullptr;
1343 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
1344 {
1345 extractTree( sub, fullPath );
1346 git_tree_free( sub );
1347 }
1348 }
1349 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1350 {
1351 git_blob* blob = nullptr;
1352 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
1353 {
1354 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
1355
1356 wxFileName dstDir( dst );
1357 dstDir.SetFullName( wxEmptyString );
1358 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1359
1360 wxFFile f( dst.GetFullPath(), wxT( "wb" ) );
1361 if( f.IsOpened() )
1362 {
1363 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1364 f.Close();
1365 }
1366 else
1367 {
1368 wxLogTrace( traceAutoSave,
1369 wxS( "[history] extractCommitToTemp: Failed to write '%s'" ),
1370 dst.GetFullPath() );
1371 extractSuccess = false;
1372 git_blob_free( blob );
1373 return;
1374 }
1375
1376 git_blob_free( blob );
1377 }
1378 }
1379 }
1380 };
1381
1382 extractTree( aTree, wxEmptyString );
1383 return extractSuccess;
1384}
1385
1386
1390void collectFilesInDirectory( const wxString& aRootPath, const wxString& aSearchPath,
1391 std::set<wxString>& aFiles )
1392{
1393 wxDir dir( aSearchPath );
1394 if( !dir.IsOpened() )
1395 return;
1396
1397 wxString filename;
1398 bool cont = dir.GetFirst( &filename );
1399
1400 while( cont )
1401 {
1402 wxFileName fullPath( aSearchPath, filename );
1403 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
1404
1405 if( fullPath.IsDir() && fullPath.DirExists() )
1406 {
1407 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
1408 }
1409 else if( fullPath.FileExists() )
1410 {
1411 aFiles.insert( relativePath );
1412 }
1413
1414 cont = dir.GetNext( &filename );
1415 }
1416}
1417
1418
1422bool shouldExcludeFromBackup( const wxString& aFilename )
1423{
1424 // Files explicitly excluded from backup should not be deleted during restore
1425 return aFilename == wxS( "fp-info-cache" );
1426}
1427
1428
1432void findFilesToDelete( const wxString& aProjectPath, const std::set<wxString>& aRestoredFiles,
1433 std::vector<wxString>& aFilesToDelete )
1434{
1435 std::function<void( const wxString&, const wxString& )> scanDirectory =
1436 [&]( const wxString& dirPath, const wxString& relativeBase )
1437 {
1438 wxDir dir( dirPath );
1439 if( !dir.IsOpened() )
1440 return;
1441
1442 wxString filename;
1443 bool cont = dir.GetFirst( &filename );
1444
1445 while( cont )
1446 {
1447 // Skip special directories
1448 if( filename == wxS(".history") || filename == wxS(".git") ||
1449 filename == wxS("_restore_backup") || filename == wxS("_restore_temp") )
1450 {
1451 cont = dir.GetNext( &filename );
1452 continue;
1453 }
1454
1455 wxFileName fullPath( dirPath, filename );
1456 wxString relativePath = relativeBase.IsEmpty() ? filename :
1457 relativeBase + wxS("/") + filename;
1458
1459 if( fullPath.IsDir() && fullPath.DirExists() )
1460 {
1461 scanDirectory( fullPath.GetFullPath(), relativePath );
1462 }
1463 else if( fullPath.FileExists() )
1464 {
1465 // Check if this file exists in the restored commit
1466 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
1467 {
1468 // Don't propose deletion of files that were never in backup scope
1469 if( !shouldExcludeFromBackup( filename ) )
1470 aFilesToDelete.push_back( relativePath );
1471 }
1472 }
1473
1474 cont = dir.GetNext( &filename );
1475 }
1476 };
1477
1478 scanDirectory( aProjectPath, wxEmptyString );
1479}
1480
1481
1486bool confirmFileDeletion( wxWindow* aParent, const std::vector<wxString>& aFilesToDelete,
1487 bool& aKeepAllFiles )
1488{
1489 if( aFilesToDelete.empty() || !aParent )
1490 {
1491 aKeepAllFiles = false;
1492 return true;
1493 }
1494
1495 wxString message = _( "The following files will be deleted when restoring this commit:\n\n" );
1496
1497 // Limit display to first 20 files to avoid overwhelming dialog
1498 size_t displayCount = std::min( aFilesToDelete.size(), size_t(20) );
1499 for( size_t i = 0; i < displayCount; ++i )
1500 {
1501 message += wxS(" • ") + aFilesToDelete[i] + wxS("\n");
1502 }
1503
1504 if( aFilesToDelete.size() > displayCount )
1505 {
1506 message += wxString::Format( _( "\n... and %zu more files\n" ),
1507 aFilesToDelete.size() - displayCount );
1508 }
1509
1510 KICAD_MESSAGE_DIALOG dlg( aParent, message, _( "Delete Files during Restore" ),
1511 wxYES_NO | wxCANCEL | wxICON_QUESTION );
1512 dlg.SetYesNoCancelLabels( _( "Proceed" ), _( "Keep All Files" ), _( "Abort" ) );
1513 dlg.SetExtendedMessage(
1514 _( "Choosing 'Keep All Files' will restore the selected commit but retain any existing "
1515 "files in the project directory. Choosing 'Proceed' will delete files that are not "
1516 "present in the restored commit." ) );
1517
1518 int choice = dlg.ShowModal();
1519
1520 if( choice == wxID_CANCEL )
1521 {
1522 wxLogTrace( traceAutoSave, wxS( "[history] User cancelled restore" ) );
1523 return false;
1524 }
1525 else if( choice == wxID_NO ) // Keep All Files
1526 {
1527 wxLogTrace( traceAutoSave, wxS( "[history] User chose to keep all files" ) );
1528 aKeepAllFiles = true;
1529 }
1530 else // Proceed with deletion
1531 {
1532 wxLogTrace( traceAutoSave, wxS( "[history] User chose to proceed with deletion" ) );
1533 aKeepAllFiles = false;
1534 }
1535
1536 return true;
1537}
1538
1539
1543bool backupCurrentFiles( const wxString& aProjectPath, const wxString& aBackupPath,
1544 const wxString& aTempRestorePath, bool aKeepAllFiles,
1545 std::set<wxString>& aBackedUpFiles )
1546{
1547 wxDir currentDir( aProjectPath );
1548 if( !currentDir.IsOpened() )
1549 return false;
1550
1551 wxString filename;
1552 bool cont = currentDir.GetFirst( &filename );
1553
1554 while( cont )
1555 {
1556 if( filename != wxS( ".history" ) && filename != wxS( ".git" ) &&
1557 filename != wxS( "_restore_backup" ) && filename != wxS( "_restore_temp" ) )
1558 {
1559 // If keepAllFiles is true, only backup files that will be overwritten
1560 bool shouldBackup = !aKeepAllFiles;
1561
1562 if( aKeepAllFiles )
1563 {
1564 // Check if this file exists in the restored commit
1565 wxFileName testPath( aTempRestorePath, filename );
1566 shouldBackup = testPath.Exists();
1567 }
1568
1569 if( shouldBackup )
1570 {
1571 wxFileName source( aProjectPath, filename );
1572 wxFileName dest( aBackupPath, filename );
1573
1574 // Create backup directory if needed
1575 if( !wxDirExists( aBackupPath ) )
1576 {
1577 wxLogTrace( traceAutoSave,
1578 wxS( "[history] backupCurrentFiles: Creating backup directory %s" ),
1579 aBackupPath );
1580 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
1581 }
1582
1583 wxLogTrace( traceAutoSave,
1584 wxS( "[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
1585 source.GetFullPath(), dest.GetFullPath() );
1586
1587 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1588 {
1589 wxLogTrace( traceAutoSave,
1590 wxS( "[history] backupCurrentFiles: Failed to backup '%s'" ),
1591 source.GetFullPath() );
1592 return false;
1593 }
1594
1595 aBackedUpFiles.insert( filename );
1596 }
1597 }
1598 cont = currentDir.GetNext( &filename );
1599 }
1600
1601 return true;
1602}
1603
1604
1608bool restoreFilesFromTemp( const wxString& aTempRestorePath, const wxString& aProjectPath,
1609 std::set<wxString>& aRestoredFiles )
1610{
1611 wxDir tempDir( aTempRestorePath );
1612 if( !tempDir.IsOpened() )
1613 return false;
1614
1615 wxString filename;
1616 bool cont = tempDir.GetFirst( &filename );
1617
1618 while( cont )
1619 {
1620 wxFileName source( aTempRestorePath, filename );
1621 wxFileName dest( aProjectPath, filename );
1622
1623 wxLogTrace( traceAutoSave,
1624 wxS( "[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
1625 source.GetFullPath(), dest.GetFullPath() );
1626
1627 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1628 {
1629 wxLogTrace( traceAutoSave,
1630 wxS( "[history] restoreFilesFromTemp: Failed to move '%s'" ),
1631 source.GetFullPath() );
1632 return false;
1633 }
1634
1635 aRestoredFiles.insert( filename );
1636 cont = tempDir.GetNext( &filename );
1637 }
1638
1639 return true;
1640}
1641
1642
1646void rollbackRestore( const wxString& aProjectPath, const wxString& aBackupPath,
1647 const wxString& aTempRestorePath, const std::set<wxString>& aBackedUpFiles,
1648 const std::set<wxString>& aRestoredFiles )
1649{
1650 wxLogTrace( traceAutoSave, wxS( "[history] rollbackRestore: Rolling back due to failure" ) );
1651
1652 // Remove ONLY the files we successfully moved from temp directory
1653 // This preserves any files that were NOT in the backup (never tracked in history)
1654 for( const wxString& filename : aRestoredFiles )
1655 {
1656 wxFileName toRemove( aProjectPath, filename );
1657 wxLogTrace( traceAutoSave, wxS( "[history] rollbackRestore: Removing '%s'" ),
1658 toRemove.GetFullPath() );
1659
1660 if( toRemove.DirExists() )
1661 {
1662 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1663 }
1664 else if( toRemove.FileExists() )
1665 {
1666 wxRemoveFile( toRemove.GetFullPath() );
1667 }
1668 }
1669
1670 // Restore from backup - put back only what we moved
1671 if( wxDirExists( aBackupPath ) )
1672 {
1673 for( const wxString& filename : aBackedUpFiles )
1674 {
1675 wxFileName source( aBackupPath, filename );
1676 wxFileName dest( aProjectPath, filename );
1677
1678 if( source.Exists() )
1679 {
1680 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1681 wxLogTrace( traceAutoSave, wxS( "[history] rollbackRestore: Restored '%s'" ),
1682 dest.GetFullPath() );
1683 }
1684 }
1685 }
1686
1687 // Clean up temporary directories
1688 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
1689 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
1690}
1691
1692
1696bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
1697 const wxString& aHash )
1698{
1699 git_time_t t = git_commit_time( aCommit );
1700 wxDateTime dt( (time_t) t );
1701 git_signature* sig = nullptr;
1702 git_signature_now( &sig, "KiCad", "[email protected]" );
1703 git_commit* parent = nullptr;
1704 git_oid parent_id;
1705
1706 if( git_reference_name_to_id( &parent_id, aRepo, "HEAD" ) == 0 )
1707 git_commit_lookup( &parent, aRepo, &parent_id );
1708
1709 wxString msg;
1710 msg.Printf( wxS( "Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1711
1712 git_oid new_id;
1713 const git_commit* constParent = parent;
1714 int result = git_commit_create( &new_id, aRepo, "HEAD", sig, sig, nullptr,
1715 msg.mb_str().data(), aTree, parent ? 1 : 0,
1716 parent ? &constParent : nullptr );
1717
1718 if( parent )
1719 git_commit_free( parent );
1720 git_signature_free( sig );
1721
1722 return result == 0;
1723}
1724
1725} // namespace
1726
1727
1728bool LOCAL_HISTORY::RestoreCommit( const wxString& aProjectPath, const wxString& aHash,
1729 wxWindow* aParent )
1730{
1731 // STEP 1: Verify no files are open by checking for LOCKFILEs
1732 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Checking for open files in %s" ),
1733 aProjectPath );
1734
1735 std::vector<wxString> lockedFiles;
1736 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
1737 {
1738 wxString lockList;
1739 for( const auto& f : lockedFiles )
1740 lockList += wxS("\n - ") + f;
1741
1742 wxLogTrace( traceAutoSave,
1743 wxS( "[history] RestoreCommit: Cannot restore - files are open:%s" ),
1744 lockList );
1745
1746 // Show user-visible warning dialog
1747 if( aParent )
1748 {
1749 wxString msg = _( "Cannot restore - the following files are open by another user:" );
1750 msg += lockList;
1751 wxMessageBox( msg, _( "Restore Failed" ), wxOK | wxICON_WARNING, aParent );
1752 }
1753 return false;
1754 }
1755
1756 // STEP 2: Acquire history lock and verify target commit
1757 HISTORY_LOCK_MANAGER lock( aProjectPath );
1758
1759 if( !lock.IsLocked() )
1760 {
1761 wxLogTrace( traceAutoSave,
1762 wxS( "[history] RestoreCommit: Failed to acquire lock for %s" ),
1763 aProjectPath );
1764 return false;
1765 }
1766
1767 git_repository* repo = lock.GetRepository();
1768 if( !repo )
1769 return false;
1770
1771 // Verify the target commit exists
1772 git_oid oid;
1773 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
1774 {
1775 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Invalid hash %s" ), aHash );
1776 return false;
1777 }
1778
1779 git_commit* commit = nullptr;
1780 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
1781 {
1782 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Commit not found %s" ), aHash );
1783 return false;
1784 }
1785
1786 git_tree* tree = nullptr;
1787 git_commit_tree( &tree, commit );
1788
1789 // Create pre-restore backup snapshot using the existing lock
1790 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Creating pre-restore backup" ) );
1791
1792 std::vector<wxString> backupFiles;
1793 collectProjectFiles( aProjectPath, backupFiles );
1794
1795 if( !backupFiles.empty() )
1796 {
1797 wxString hist = historyPath( aProjectPath );
1798 if( !commitSnapshotWithLock( repo, lock.GetIndex(), hist, aProjectPath, backupFiles,
1799 wxS( "Pre-restore backup" ) ) )
1800 {
1801 wxLogTrace( traceAutoSave,
1802 wxS( "[history] RestoreCommit: Failed to create pre-restore backup" ) );
1803 git_tree_free( tree );
1804 git_commit_free( commit );
1805 return false;
1806 }
1807 }
1808
1809 // STEP 3: Extract commit to temporary location
1810 wxString tempRestorePath = aProjectPath + wxS("_restore_temp");
1811
1812 if( wxDirExists( tempRestorePath ) )
1813 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1814
1815 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
1816 {
1817 wxLogTrace( traceAutoSave,
1818 wxS( "[history] RestoreCommit: Failed to create temp directory %s" ),
1819 tempRestorePath );
1820 git_tree_free( tree );
1821 git_commit_free( commit );
1822 return false;
1823 }
1824
1825 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Extracting to temp location %s" ),
1826 tempRestorePath );
1827
1828 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
1829 {
1830 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Extraction failed, cleaning up" ) );
1831 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1832 git_tree_free( tree );
1833 git_commit_free( commit );
1834 return false;
1835 }
1836
1837 // STEP 4: Determine which files will be deleted and ask for confirmation
1838 std::set<wxString> restoredFiles;
1839 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
1840
1841 std::vector<wxString> filesToDelete;
1842 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
1843
1844 bool keepAllFiles = true;
1845 if( !confirmFileDeletion( aParent, filesToDelete, keepAllFiles ) )
1846 {
1847 // User cancelled
1848 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1849 git_tree_free( tree );
1850 git_commit_free( commit );
1851 return false;
1852 }
1853
1854 // STEP 5: Perform atomic swap - backup current, move temp to current
1855 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Performing atomic swap" ) );
1856
1857 wxString backupPath = aProjectPath + wxS("_restore_backup");
1858
1859 // Remove old backup if exists
1860 if( wxDirExists( backupPath ) )
1861 {
1862 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Removing old backup %s" ),
1863 backupPath );
1864 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1865 }
1866
1867 // Track which files we moved to backup and restored (for rollback)
1868 std::set<wxString> backedUpFiles;
1869 std::set<wxString> restoredFilesSet;
1870
1871 // Backup current files
1872 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
1873 backedUpFiles ) )
1874 {
1875 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1876 restoredFilesSet );
1877 git_tree_free( tree );
1878 git_commit_free( commit );
1879 return false;
1880 }
1881
1882 // Restore files from temp
1883 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
1884 {
1885 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1886 restoredFilesSet );
1887 git_tree_free( tree );
1888 git_commit_free( commit );
1889 return false;
1890 }
1891
1892 // SUCCESS - Clean up temp and backup directories
1893 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Restore successful, cleaning up" ) );
1894 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1895 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1896
1897 // Record the restore in history
1898 recordRestoreInHistory( repo, commit, tree, aHash );
1899
1900 git_tree_free( tree );
1901 git_commit_free( commit );
1902
1903 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Complete" ) );
1904 return true;
1905}
1906
1907void LOCAL_HISTORY::ShowRestoreDialog( const wxString& aProjectPath, wxWindow* aParent )
1908{
1909 if( !HistoryExists( aProjectPath ) )
1910 return;
1911
1912 wxString hist = historyPath( aProjectPath );
1913 git_repository* repo = nullptr;
1914
1915 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1916 return;
1917
1918 git_revwalk* walk = nullptr;
1919 git_revwalk_new( &walk, repo );
1920 git_revwalk_push_head( walk );
1921
1922 std::vector<wxString> choices;
1923 std::vector<wxString> hashes;
1924 git_oid oid;
1925
1926 while( git_revwalk_next( &oid, walk ) == 0 )
1927 {
1928 git_commit* commit = nullptr;
1929 git_commit_lookup( &commit, repo, &oid );
1930
1931 git_time_t t = git_commit_time( commit );
1932 wxDateTime dt( (time_t) t );
1933 wxString line;
1934
1935 line.Printf( wxS( "%s %s" ), dt.FormatISOCombined().c_str(),
1936 wxString::FromUTF8( git_commit_summary( commit ) ) );
1937 choices.push_back( line );
1938 hashes.push_back( wxString::FromUTF8( git_oid_tostr_s( &oid ) ) );
1939 git_commit_free( commit );
1940 }
1941
1942 git_revwalk_free( walk );
1943 git_repository_free( repo );
1944
1945 if( choices.empty() )
1946 return;
1947
1948 int index = wxGetSingleChoiceIndex( _( "Select snapshot" ), _( "Restore" ),
1949 (int) choices.size(), &choices[0], aParent );
1950
1951 if( index != wxNOT_FOUND )
1952 RestoreCommit( aProjectPath, hashes[index] );
1953}
1954
int index
const char * name
Hybrid locking mechanism for local history git repositories.
git_repository * GetRepository()
Get the git repository handle (only valid if IsLocked() returns true).
wxString GetLockError() const
Get error message describing why lock could not be acquired.
git_index * GetIndex()
Get the git index handle (only valid if IsLocked() returns true).
bool IsLocked() const
Check if locks were successfully acquired.
bool TagSave(const wxString &aProjectPath, const wxString &aFileType)
Tag a manual save in the local history repository.
wxString GetHeadHash(const wxString &aProjectPath)
Return the current head commit hash.
bool RestoreCommit(const wxString &aProjectPath, const wxString &aHash, wxWindow *aParent=nullptr)
Restore the project files to the state recorded by the given commit hash.
void ShowRestoreDialog(const wxString &aProjectPath, wxWindow *aParent)
Show a dialog allowing the user to choose a snapshot to restore.
bool HeadNewerThanLastSave(const wxString &aProjectPath)
Return true if the autosave data is newer than the last manual save.
std::set< wxString > m_pendingFiles
bool CommitDuplicateOfLastSave(const wxString &aProjectPath, const wxString &aFileType, const wxString &aMessage)
Create a new commit duplicating the tree pointed to by Last_Save_<fileType> and move the Last_Save_<f...
bool EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
void ClearAllSavers()
Clear all registered savers.
bool CommitSnapshot(const std::vector< wxString > &aFiles, const wxString &aTitle)
Commit the given files to the local history repository.
std::map< const void *, std::function< void(const wxString &, std::vector< wxString > &)> > m_savers
bool RunRegisteredSaversAndCommit(const wxString &aProjectPath, const wxString &aTitle)
Run all registered savers and, if any staged changes differ from HEAD, create a commit.
void NoteFileChange(const wxString &aFile)
Record that a file has been modified and should be included in the next snapshot.
bool CommitPending()
Commit any pending modified files to the history repository.
bool HistoryExists(const wxString &aProjectPath)
Return true if history exists for the project.
bool CommitFullProjectSnapshot(const wxString &aProjectPath, const wxString &aTitle)
Commit a snapshot of the entire project directory (excluding the .history directory and ignored trans...
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< wxString > &)> &aSaver)
Register a saver callback invoked during autosave history commits.
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
Definition confirm.h:52
#define _(s)
static const std::string LockFileExtension
static const std::string LockFilePrefix
const wxChar *const traceAutoSave
Flag to enable auto save feature debug tracing.
static wxString historyPath(const wxString &aProjectPath)
static wxString historyPath(const wxString &aProjectPath)
static size_t dirSizeRecursive(const wxString &path)
static bool commitSnapshotWithLock(git_repository *repo, git_index *index, const wxString &aHistoryPath, const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
static void collectProjectFiles(const wxString &aProjectPath, std::vector< wxString > &aFiles)
File locking utilities.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
std::string path
wxString result
Test unit parsing edge cases and error handling.
int delta
wxLogTrace helper definitions.
Definition of file extensions used in Kicad.