38#include <wx/filename.h>
42#include <wx/datetime.h>
66 const wxString& aRelativePath )
68 wxFileName fn( aRelativePath );
71 return fn.GetFullPath();
76 wxArrayString dirs = fn.GetDirs();
79 dst.AssignDir( aHistoryRoot );
81 for(
const wxString& d : dirs )
84 dst.SetFullName( fn.GetFullName() );
85 return dst.GetFullPath();
95 wxFFile fileA( aPathA, wxS(
"rb" ) );
96 wxFFile fileB( aPathB, wxS(
"rb" ) );
98 if( !fileA.IsOpened() || !fileB.IsOpened() )
101 wxFileOffset lenA = fileA.Length();
102 wxFileOffset lenB = fileB.Length();
104 if( lenA < 0 || lenB < 0 || lenA != lenB )
107 constexpr size_t chunkSize = 64 * 1024;
108 std::vector<char> bufA( chunkSize );
109 std::vector<char> bufB( chunkSize );
111 while( !fileA.Eof() )
113 size_t readA = fileA.Read( bufA.data(), chunkSize );
114 size_t readB = fileB.Read( bufB.data(), chunkSize );
119 if( readA > 0 && std::memcmp( bufA.data(), bufB.data(), readA ) != 0 )
122 if( fileA.Error() || fileB.Error() )
136 const wxString& aRelativePath,
139 wxFileName rel( aRelativePath );
141 dst.AssignDir( aAutosaveRoot );
143 for(
const wxString& d : rel.GetDirs() )
149 dst.SetFullName( rel.GetFullName() );
151 return dst.GetFullPath();
159 const wxString& aProjectPath,
160 const wxString& aAutosaveRoot,
163 wxFileName autosave( aAutosavePath );
167 wxString
name = autosave.GetFullName();
170 return wxEmptyString;
173 return autosave.GetFullPath();
176 if( !aAutosavePath.StartsWith( aAutosaveRoot ) )
177 return wxEmptyString;
179 wxString rel = aAutosavePath.Mid( aAutosaveRoot.length() );
180 wxFileName projFn( aProjectPath, wxEmptyString );
182 return projFn.GetPathWithSep() + rel;
187 const wxString& aTitle );
208 if( aProjectPath.IsEmpty() || !wxDirExists( aProjectPath ) )
211 wxDir dir( aProjectPath );
214 return dir.IsOpened()
225 return aName == wxS(
".history" )
226 || aName == wxS(
".git" )
227 || aName == wxS(
"_restore_backup" )
228 || aName.StartsWith( wxS(
"_restore_backup_" ) )
229 || aName == wxS(
"_restore_temp" )
244 wxFileName fn( aFile );
246 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
254 const void* aSaverObject,
255 const std::function<
void(
const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
259 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping" ), aSaverObject );
264 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)" ), aSaverObject,
m_savers.size() );
272 auto it =
m_savers.find( aSaverObject );
277 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)" ),
287 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers" ) );
292 const wxString& aTagFileType )
294 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
296 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
302 wxLogTrace(
traceAutoSave, wxS(
"[history] Backup format is ZIP; skipping git commit" ) );
309 Init( aProjectPath );
312 wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu tag='%s'" ),
313 aProjectPath, aTitle,
m_savers.size(), aTagFileType );
317 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
322 if( !aTagFileType.IsEmpty() )
328 wxLogTrace(
traceAutoSave, wxS(
"[history] previous save still in progress; skipping cycle" ) );
333 std::vector<HISTORY_FILE_DATA> fileData;
335 for(
const auto& [saverObject, saver] :
m_savers )
337 size_t before = fileData.size();
338 saver( aProjectPath, fileData );
339 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p produced %zu entries (total=%zu)" ),
340 saverObject, fileData.size() - before, fileData.size() );
346 fileData.erase( std::remove_if( fileData.begin(), fileData.end(),
349 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
351 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out entry with invalid path: '%s'" ),
352 entry.relativePath );
359 if( fileData.empty() )
361 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no entries; skipping" ) );
366 m_saveInProgress.store(
true, std::memory_order_release );
369 [
this, projectPath = aProjectPath, title = aTitle, tagFileType = aTagFileType,
370 data = std::move( fileData )]()
mutable ->
bool
372 bool result = commitInBackground( projectPath, title, data, !tagFileType.IsEmpty() );
374 if( !tagFileType.IsEmpty() )
375 TagSave( projectPath, tagFileType );
377 m_saveInProgress.store(
false, std::memory_order_release );
382 if( !aTagFileType.IsEmpty() )
383 WaitForPendingSave();
391 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
396 wxLogTrace(
traceAutoSave, wxS(
"[autosave] no savers registered; skipping" ) );
406 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create autosave root '%s'" ), autosaveRoot );
410 std::vector<HISTORY_FILE_DATA> fileData;
412 for(
const auto& [saverObject, saver] :
m_savers )
413 saver( aProjectPath, fileData );
415 bool anyWritten =
false;
419 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
423 wxFileName dstFn( dst );
427 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create dir '%s'" ), dstFn.GetPath() );
433 if( !entry.content.empty() )
435 buf = std::move( entry.content );
440 else if( !entry.sourcePath.IsEmpty() )
442 wxFFile src( entry.sourcePath, wxS(
"rb" ) );
444 if( !src.IsOpened() )
447 wxFileOffset len = src.Length();
452 buf.resize(
static_cast<size_t>( len ) );
454 if( len > 0 && src.Read( buf.data(), buf.size() ) != buf.size() )
470 wxLogTrace(
traceAutoSave, wxS(
"[autosave] wrote %zu bytes to '%s'" ), buf.size(), dst );
474 wxLogTrace(
traceAutoSave, wxS(
"[autosave] write failed for '%s': %s" ), dst, err );
487static std::vector<std::pair<wxString, wxString>>
490 std::vector<std::pair<wxString, wxString>> results;
496 if( !wxDirExists( autosaveRoot ) )
499 std::function<void(
const wxString& )> walk = [&](
const wxString& aDir )
507 bool cont = d.GetFirst( &
name );
511 wxFileName fn( aDir,
name );
512 wxString fullPath = fn.GetFullPath();
514 if( wxDirExists( fullPath ) )
517 && (
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) ) )
519 cont = d.GetNext( &
name );
532 results.emplace_back( fullPath, src );
535 cont = d.GetNext( &
name );
539 walk( autosaveRoot );
544std::vector<std::pair<wxString, wxString>>
547 std::vector<std::pair<wxString, wxString>> results;
549 if( aExtensions.empty() )
554 wxFileName srcFn( pair.second );
557 for(
const wxString& ext : aExtensions )
559 if( srcFn.GetExt().IsSameAs( ext,
false ) )
571 if( srcFn.FileExists() )
572 srcTime = srcFn.GetModificationTime();
574 wxDateTime autosaveTime = wxFileName( pair.first ).GetModificationTime();
578 bool stale = !srcTime.IsValid()
579 || ( autosaveTime.IsLaterThan( srcTime )
583 results.emplace_back( std::move( pair ) );
597 if( wxFileExists( autosavePath ) )
598 wxRemoveFile( autosavePath );
604 const std::vector<wxString>& aSourcePaths )
const
606 if( aSourcePaths.empty() )
609 std::vector<wxFileName> targets;
610 targets.reserve( aSourcePaths.size() );
612 for(
const wxString& src : aSourcePaths )
615 targets.emplace_back( src );
618 if( targets.empty() )
623 wxFileName srcFn( srcPath );
626 for(
const wxFileName& target : targets )
628 if( srcFn.SameAs( target ) )
635 if( match && wxFileExists( autosavePath ) )
636 wxRemoveFile( autosavePath );
642 const std::vector<HISTORY_FILE_DATA>& aFileData,
bool aIsManualSave )
644 wxLogTrace(
traceAutoSave, wxS(
"[history] background: writing %zu entries for '%s'" ),
645 aFileData.size(), aProjectPath );
651 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create history root '%s'" ), hist );
658 wxFileName dstFn( dst );
659 wxString parent = dstFn.GetPath();
663 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create dir '%s'" ), parent );
667 if( !entry.content.empty() )
669 std::string buf = entry.content;
674 wxFFile fp( dst, wxS(
"wb" ) );
678 fp.Write( buf.data(), buf.size() );
680 wxLogTrace(
traceAutoSave, wxS(
"[history] background: wrote %zu bytes to '%s'" ), buf.size(), dst );
684 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to open '%s' for writing" ), dst );
687 else if( !entry.sourcePath.IsEmpty() )
689 wxCopyFile( entry.sourcePath, dst,
true );
690 wxLogTrace(
traceAutoSave, wxS(
"[history] background: copied '%s' -> '%s'" ), entry.sourcePath, dst );
706 git_repository_set_workdir( repo, hist.mb_str().data(),
false );
712 wxString rel = entry.relativePath;
713 rel.Replace( wxS(
"\\" ), wxS(
"/" ) );
717 if( !wxFileExists( abs ) )
720 git_index_add_bypath(
index, rel.ToStdString().c_str() );
725 git_commit* head_commit =
nullptr;
726 git_tree* head_tree =
nullptr;
728 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
729 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
730 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
732 git_tree* rawIndexTree =
nullptr;
733 git_oid index_tree_oid;
735 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
738 git_tree_free( head_tree );
741 git_commit_free( head_commit );
743 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to write index tree" ) );
747 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
748 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
750 bool hasChanges =
true;
754 git_diff* diff =
nullptr;
756 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
758 hasChanges = git_diff_num_deltas( diff ) > 0;
759 wxLogTrace(
traceAutoSave, wxS(
"[history] background: diff deltas=%u" ),
760 (
unsigned) git_diff_num_deltas( diff ) );
761 git_diff_free( diff );
768 bool stagedMatchesDisk =
true;
772 wxString diskPath = aProjectPath + wxFileName::GetPathSeparator() + entry.relativePath;
775 if( !wxFileExists( diskPath ) || !wxFileExists( histPath ) )
777 stagedMatchesDisk =
false;
781 wxFFile diskFile( diskPath, wxT(
"rb" ) );
782 wxFFile histFile( histPath, wxT(
"rb" ) );
784 if( !diskFile.IsOpened() || !histFile.IsOpened() || diskFile.Length() != histFile.Length() )
786 stagedMatchesDisk =
false;
790 size_t len =
static_cast<size_t>( diskFile.Length() );
791 std::string diskBuf( len,
'\0' );
792 std::string histBuf( len,
'\0' );
794 if( diskFile.Read( diskBuf.data(), len ) != len
795 || histFile.Read( histBuf.data(), len ) != len
796 || diskBuf != histBuf )
798 stagedMatchesDisk =
false;
803 if( stagedMatchesDisk && !aIsManualSave )
805 wxLogTrace(
traceAutoSave, wxS(
"[history] background: first commit; staged matches disk -- skipping" ) );
811 git_tree_free( head_tree );
814 git_commit_free( head_commit );
818 wxLogTrace(
traceAutoSave, wxS(
"[history] background: no changes detected; no commit") );
822 if( !aTitle.IsEmpty() && aTitle != wxS(
"Autosave" ) )
824 git_oid head_oid_amend;
826 if( git_reference_name_to_id( &head_oid_amend, repo,
"HEAD" ) == 0 )
828 git_commit* head_commit_amend =
nullptr;
830 if( git_commit_lookup( &head_commit_amend, repo, &head_oid_amend ) == 0 )
832 wxString existingMsg = wxString::FromUTF8( git_commit_message( head_commit_amend ) );
833 existingMsg.Trim(
true ).Trim(
false );
835 if( existingMsg != aTitle )
838 int amend_rc = git_commit_amend( &amended_oid, head_commit_amend,
"HEAD",
nullptr,
nullptr,
839 nullptr, aTitle.mb_str().data(),
nullptr );
842 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amended HEAD message '%s' -> '%s'" ),
843 existingMsg, aTitle );
845 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amend failed rc=%d" ), amend_rc );
848 git_commit_free( head_commit_amend );
856 git_signature* rawSig =
nullptr;
858 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
860 git_commit* parent =
nullptr;
864 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
866 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
870 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
872 const git_commit* constParent = parent;
874 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
875 msg.mb_str().data(), indexTree.get(), parents,
876 parents ? &constParent :
nullptr );
880 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit created %s (%s entries=%zu)" ),
881 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
885 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit failed rc=%d" ), rc );
889 git_commit_free( parent );
891 git_index_write(
index );
900 wxLogTrace(
traceAutoSave, wxS(
"[history] waiting for pending background save" ) );
924 if( !wxDirExists( hist ) )
933 git_repository* rawRepo =
nullptr;
935 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
937 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
940 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
941 if( !ignoreFile.FileExists() )
943 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
946 f.Write( wxS(
"# KiCad local history exclusions. Edit to add your own rules.\n"
953 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
955 if( !readmeFile.FileExists() )
957 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
961 f.Write( wxS(
"KiCad Local History Directory\n"
962 "=============================\n\n"
963 "This directory contains automatic snapshots of your project files.\n"
964 "KiCad periodically saves copies of your work here, allowing you to\n"
965 "recover from accidental changes or data loss.\n\n"
966 "You can browse and restore previous versions through KiCad's\n"
967 "File > Local History menu.\n\n"
968 "To disable this feature:\n"
969 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
970 "This directory can be safely deleted if you no longer need the\n"
971 "history, but doing so will permanently remove all saved snapshots.\n" ) );
977 git_repository_free( rawRepo );
993 const wxString& aHistoryPath,
const wxString& aProjectPath,
994 const std::vector<wxString>& aFiles,
const wxString& aTitle )
996 std::vector<std::string> filesArrStr;
998 for(
const wxString& file : aFiles )
1000 wxFileName src( file );
1003 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
1004 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
1006 relPath = src.GetFullName();
1008 relPath.Replace(
"\\",
"/" );
1009 std::string relPathStr = relPath.ToStdString();
1011 unsigned int status = 0;
1012 int rc = git_status_file( &status, repo, relPathStr.data() );
1014 if( rc == 0 && status != 0 )
1016 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
1017 filesArrStr.emplace_back( relPathStr );
1021 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
1022 filesArrStr.emplace_back( relPathStr );
1026 std::vector<char*> cStrings( filesArrStr.size() );
1028 for(
size_t i = 0; i < filesArrStr.size(); i++ )
1029 cStrings[i] = filesArrStr[i].data();
1031 git_strarray filesArrGit;
1032 filesArrGit.count = filesArrStr.size();
1033 filesArrGit.strings = cStrings.data();
1035 if( filesArrStr.size() == 0 )
1041 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
1043 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
1049 if( git_index_write_tree( &tree_id,
index ) != 0 )
1052 git_tree* rawTree =
nullptr;
1053 git_tree_lookup( &rawTree, repo, &tree_id );
1054 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
1056 git_signature* rawSig =
nullptr;
1058 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
1059 &git_signature_free );
1061 git_commit* rawParent =
nullptr;
1065 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
1067 git_commit_lookup( &rawParent, repo, &parent_id );
1071 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
1074 git_tree* rawParentTree =
nullptr;
1077 git_commit_tree( &rawParentTree, parent.get() );
1079 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
1081 git_diff* rawDiff =
nullptr;
1082 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
1083 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
1085 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
1087 if( numChangedFiles == 0 )
1089 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
1095 if( !aTitle.IsEmpty() )
1096 msg << aTitle << wxS(
": " );
1098 msg << numChangedFiles << wxS(
" files changed" );
1100 for(
size_t i = 0; i < numChangedFiles; ++i )
1102 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
1103 git_patch* rawPatch =
nullptr;
1104 git_patch_from_diff( &rawPatch, diff.get(), i );
1105 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
1107 size_t context = 0, adds = 0, dels = 0;
1108 git_patch_line_stats( &context, &adds, &dels, patch.get() );
1109 size_t updated = std::min( adds, dels );
1112 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
1113 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
1117 git_commit* parentPtr = parent.get();
1118 const git_commit* constParentPtr = parentPtr;
1119 if( git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr, msg.mb_str().data(), tree.get(),
1120 parents, parentPtr ? &constParentPtr :
nullptr )
1126 git_index_write(
index );
1135 const wxString& aTitle )
1143 wxLogTrace(
traceAutoSave, wxS(
"[history] commitSnapshotForProject failed to acquire lock: %s" ),
1155 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled
1161 wxString proj = wxFileName( aFiles[0] ).GetPath();
1175 wxString
name = aFile.GetFullName();
1177 if(
name == wxS(
"sym-lib-table" ) ||
name == wxS(
"fp-lib-table" ) )
1180 return aFile.GetExt().StartsWith( wxS(
"kicad_" ) );
1189 wxDir dir( aProjectPath );
1191 if( !dir.IsOpened() )
1195 std::function<void(
const wxString&,
bool )> collect =
1196 [&](
const wxString&
path,
bool topLevel )
1201 wxS(
"[history] collectProjectFiles: Skipping nested project at %s" ),
1212 bool cont = d.GetFirst( &
name );
1218 cont = d.GetNext( &
name );
1223 wxString fullPath = fn.GetFullPath();
1225 if( wxFileName::DirExists( fullPath ) )
1227 collect( fullPath,
false );
1229 else if( fn.FileExists() && fn.GetFullName() != wxS(
"fp-info-cache" ) &&
isKiCadProjectFile( fn ) )
1231 aFiles.push_back( fn.GetFullPath() );
1234 cont = d.GetNext( &
name );
1238 collect( aProjectPath,
true );
1249 wxLogTrace(
traceAutoSave, wxS(
"[history] Backup format is ZIP; skipping full snapshot" ) );
1253 std::vector<wxString> files;
1259 Init( aProjectPath );
1265 return wxDirExists(
historyPath( aProjectPath ) );
1280 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
1290 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
1295 git_reference* ref =
nullptr;
1298 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
1299 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
1302 git_object* head_obj =
nullptr;
1303 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
1304 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
1305 git_object_free( head_obj );
1308 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
1309 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
1311 git_reference_delete( ref );
1312 git_reference_free( ref );
1315 git_oid last_tag_oid;
1316 git_object* head_obj2 =
nullptr;
1317 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
1318 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
1319 git_object_free( head_obj2 );
1327 git_repository* repo =
nullptr;
1329 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1333 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1335 git_repository_free( repo );
1339 git_commit* head_commit =
nullptr;
1340 git_commit_lookup( &head_commit, repo, &head_oid );
1341 git_time_t head_time = git_commit_time( head_commit );
1344 git_tag_list_match( &tags,
"Last_Save_*", repo );
1345 git_time_t save_time = 0;
1347 for(
size_t i = 0; i < tags.count; ++i )
1349 git_reference* ref =
nullptr;
1350 if( git_reference_lookup( &ref, repo,
1351 ( wxS(
"refs/tags/" ) +
1352 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
1354 const git_oid* oid = git_reference_target( ref );
1355 git_commit* c =
nullptr;
1356 if( git_commit_lookup( &c, repo, oid ) == 0 )
1358 git_time_t t = git_commit_time( c );
1361 git_commit_free( c );
1363 git_reference_free( ref );
1367 git_strarray_free( &tags );
1368 git_commit_free( head_commit );
1369 git_repository_free( repo );
1373 if( save_time == 0 )
1376 return head_time > save_time;
1380 const wxString& aMessage )
1392 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
1401 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
1402 git_reference* lastRef =
nullptr;
1403 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
1405 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
1407 const git_oid* lastOid = git_reference_target( lastRef );
1408 git_commit* lastCommit =
nullptr;
1409 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
1411 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
1413 git_tree* lastTree =
nullptr;
1414 git_commit_tree( &lastTree, lastCommit );
1415 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
1419 git_commit* headCommit =
nullptr;
1421 const git_commit* parentArray[1];
1422 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
1423 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
1425 parentArray[0] = headCommit;
1429 git_signature* sigRaw =
nullptr;
1431 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
1433 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
1434 git_oid newCommitOid;
1435 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
1436 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
1437 if( headCommit ) git_commit_free( headCommit );
1442 git_reference* existing =
nullptr;
1443 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
1445 git_reference_delete( existing );
1446 git_reference_free( existing );
1448 git_object* newCommitObj =
nullptr;
1449 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
1451 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
1452 git_object_free( newCommitObj );
1461 if( !dir.IsOpened() )
1464 bool cont = dir.GetFirst( &
name );
1468 wxString fullPath = fn.GetFullPath();
1470 if( wxFileName::DirExists( fullPath ) )
1472 else if( fn.FileExists() )
1473 total += (size_t) fn.GetSize().GetValue();
1474 cont = dir.GetNext( &
name );
1480static bool copyTreeObjects( git_repository* aSrcRepo, git_odb* aSrcOdb, git_odb* aDstOdb,
const git_oid* aTreeOid,
1481 std::set<git_oid,
bool ( * )(
const git_oid&,
const git_oid& )>& aCopied )
1483 if( aCopied.count( *aTreeOid ) )
1486 git_odb_object* obj =
nullptr;
1488 if( git_odb_read( &obj, aSrcOdb, aTreeOid ) != 0 )
1492 int err = git_odb_write( &written, aDstOdb, git_odb_object_data( obj ), git_odb_object_size( obj ),
1493 git_odb_object_type( obj ) );
1494 git_odb_object_free( obj );
1499 aCopied.insert( *aTreeOid );
1501 git_tree* tree =
nullptr;
1503 if( git_tree_lookup( &tree, aSrcRepo, aTreeOid ) != 0 )
1506 size_t cnt = git_tree_entrycount( tree );
1508 for(
size_t i = 0; i < cnt; ++i )
1510 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1511 const git_oid* entryId = git_tree_entry_id( entry );
1513 if( aCopied.count( *entryId ) )
1516 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1518 if( !
copyTreeObjects( aSrcRepo, aSrcOdb, aDstOdb, entryId, aCopied ) )
1520 git_tree_free( tree );
1524 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1526 git_odb_object* blobObj =
nullptr;
1528 if( git_odb_read( &blobObj, aSrcOdb, entryId ) == 0 )
1530 git_oid blobWritten;
1532 if( git_odb_write( &blobWritten, aDstOdb, git_odb_object_data( blobObj ),
1533 git_odb_object_size( blobObj ), git_odb_object_type( blobObj ) )
1536 git_odb_object_free( blobObj );
1537 git_tree_free( tree );
1541 git_odb_object_free( blobObj );
1542 aCopied.insert( *entryId );
1547 git_tree_free( tree );
1556 git_packbuilder* pb =
nullptr;
1558 if( git_packbuilder_new( &pb, aRepo ) != 0 )
1561 git_revwalk* walk =
nullptr;
1563 if( git_revwalk_new( &walk, aRepo ) != 0 )
1565 git_packbuilder_free( pb );
1569 git_revwalk_push_head( walk );
1572 while( git_revwalk_next( &oid, walk ) == 0 )
1574 if( git_packbuilder_insert_commit( pb, &oid ) != 0 )
1576 git_revwalk_free( walk );
1577 git_packbuilder_free( pb );
1582 git_revwalk_free( walk );
1586 git_packbuilder_set_callbacks(
1588 [](
int aStage, uint32_t aCurrent, uint32_t aTotal,
void* aPayload )
1593 reporter->SetCurrentProgress( (
double) aCurrent / aTotal );
1601 if( git_packbuilder_write( pb,
nullptr, 0,
nullptr,
nullptr ) != 0 )
1603 git_packbuilder_free( pb );
1607 git_packbuilder_free( pb );
1609 wxString objPath = wxString::FromUTF8( git_repository_path( aRepo ) ) + wxS(
"objects" );
1610 wxDir objDir( objPath );
1612 if( objDir.IsOpened() )
1614 wxArrayString toRemove;
1616 bool cont = objDir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS );
1620 if(
name.length() == 2 )
1621 toRemove.Add( objPath + wxFileName::GetPathSeparator() +
name );
1623 cont = objDir.GetNext( &
name );
1626 for(
const wxString& dir : toRemove )
1627 wxFileName::Rmdir( dir, wxPATH_RMDIR_RECURSIVE );
1636 if( aMaxBytes == 0 )
1641 if( !wxDirExists( hist ) )
1646 if( current <= aMaxBytes )
1653 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
1663 aReporter->
Report(
_(
"Compacting local history..." ) );
1670 if( current <= aMaxBytes )
1674 git_revwalk* walk =
nullptr;
1675 git_revwalk_new( &walk, repo );
1676 git_revwalk_sorting( walk, GIT_SORT_TIME );
1677 git_revwalk_push_head( walk );
1678 std::vector<git_oid> commits;
1681 while( git_revwalk_next( &oid, walk ) == 0 )
1682 commits.push_back( oid );
1684 git_revwalk_free( walk );
1686 if( commits.empty() )
1690 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
1691 [](
const git_oid& a,
const git_oid& b )
1693 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1696 size_t keptBytes = 0;
1697 std::vector<git_oid> keep;
1699 git_odb* odb =
nullptr;
1700 git_repository_odb( &odb, repo );
1702 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
1705 size_t cnt = git_tree_entrycount( tree );
1707 for(
size_t i = 0; i < cnt; ++i )
1709 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1711 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1713 const git_oid* bid = git_tree_entry_id( entry );
1715 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1718 git_object_t type = GIT_OBJECT_ANY;
1720 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1723 seenBlobs.insert( *bid );
1726 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1728 git_tree* sub =
nullptr;
1730 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1732 added += accountTree( sub );
1733 git_tree_free( sub );
1741 for(
const git_oid& cOid : commits )
1743 git_commit* c =
nullptr;
1745 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1748 git_tree* tree =
nullptr;
1749 git_commit_tree( &tree, c );
1750 size_t add = accountTree( tree );
1751 git_tree_free( tree );
1752 git_commit_free( c );
1754 if( keep.empty() || keptBytes + add <= aMaxBytes )
1756 keep.push_back( cOid );
1764 keep.push_back( commits.front() );
1768 std::vector<std::pair<wxString, git_oid>> tagTargets;
1769 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
1770 [](
const git_oid& a,
const git_oid& b )
1772 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1774 git_strarray tagList;
1776 if( git_tag_list( &tagList, repo ) == 0 )
1778 for(
size_t i = 0; i < tagList.count; ++i )
1780 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1781 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1783 git_reference* tref =
nullptr;
1785 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1787 const git_oid* toid = git_reference_target( tref );
1791 tagTargets.emplace_back(
name, *toid );
1792 taggedCommits.insert( *toid );
1796 for(
const auto& k : keep )
1798 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1808 keep.push_back( *toid );
1809 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1814 git_reference_free( tref );
1818 git_strarray_free( &tagList );
1822 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1823 wxString trimPath = trimFn.GetPath();
1825 if( wxDirExists( trimPath ) )
1826 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1828 wxMkdir( trimPath );
1829 git_repository* newRepo =
nullptr;
1831 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1833 git_odb_free( odb );
1837 git_odb* dstOdb =
nullptr;
1839 if( git_repository_odb( &dstOdb, newRepo ) != 0 )
1841 git_repository_free( newRepo );
1842 git_odb_free( odb );
1846 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> copiedObjects(
1847 [](
const git_oid& a,
const git_oid& b )
1849 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1853 std::reverse( keep.begin(), keep.end() );
1854 git_commit* parent =
nullptr;
1855 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1856 std::vector<MAP_ENTRY> commitMap;
1860 aReporter->
AdvancePhase(
_(
"Trimming local history..." ) );
1864 for(
size_t idx = 0; idx < keep.size(); ++idx )
1869 const git_oid& co = keep[idx];
1870 git_commit* orig =
nullptr;
1872 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1875 git_tree* tree =
nullptr;
1876 git_commit_tree( &tree, orig );
1878 copyTreeObjects( repo, odb, dstOdb, git_tree_id( tree ), copiedObjects );
1880 git_tree* newTree =
nullptr;
1881 git_tree_lookup( &newTree, newRepo, git_tree_id( tree ) );
1883 git_tree_free( tree );
1886 const git_signature* origAuthor = git_commit_author( orig );
1887 const git_signature* origCommitter = git_commit_committer( orig );
1888 git_signature* sigAuthor =
nullptr;
1889 git_signature* sigCommitter =
nullptr;
1891 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1892 origAuthor->when.time, origAuthor->when.offset );
1893 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1894 origCommitter->when.time, origCommitter->when.offset );
1896 const git_commit* parents[1];
1897 int parentCount = 0;
1901 parents[0] = parent;
1905 git_oid newCommitOid;
1906 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1907 newTree, parentCount, parentCount ? parents :
nullptr );
1910 git_commit_free( parent );
1912 git_commit_lookup( &parent, newRepo, &newCommitOid );
1914 commitMap.emplace_back( co, newCommitOid );
1916 git_signature_free( sigAuthor );
1917 git_signature_free( sigCommitter );
1918 git_tree_free( newTree );
1919 git_commit_free( orig );
1923 git_commit_free( parent );
1926 for(
const auto& tt : tagTargets )
1929 const git_oid* newOid =
nullptr;
1931 for(
const auto& m : commitMap )
1933 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1943 git_object* obj =
nullptr;
1945 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1947 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1948 git_object_free( obj );
1953 aReporter->
AdvancePhase(
_(
"Compacting trimmed history..." ) );
1960 git_odb_free( dstOdb );
1961 git_odb_free( odb );
1962 git_repository_free( newRepo );
1967 wxString backupOld = hist + wxS(
"_old");
1968 wxRenameFile( hist, backupOld );
1969 wxRenameFile( trimPath, hist );
1970 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1977 git_repository* repo =
nullptr;
1979 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1980 return wxEmptyString;
1983 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1985 git_repository_free( repo );
1986 return wxEmptyString;
1989 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1990 git_repository_free( repo );
2002bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
2004 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
2006 wxDir dir( dirPath );
2007 if( !dir.IsOpened() )
2011 bool cont = dir.GetFirst( &filename );
2015 wxFileName fullPath( dirPath, filename );
2018 if( filename == wxS(
".history") || filename == wxS(
".git") )
2020 cont = dir.GetNext( &filename );
2024 if( fullPath.DirExists() )
2026 findLocks( fullPath.GetFullPath() );
2028 else if( fullPath.FileExists()
2035 baseName = baseName.BeforeLast(
'.' );
2036 wxFileName originalFile( dirPath, baseName );
2039 LOCKFILE testLock( originalFile.GetFullPath() );
2040 if( testLock.Valid() && !testLock.IsLockedByMe() )
2042 aLockedFiles.push_back( fullPath.GetFullPath() );
2046 cont = dir.GetNext( &filename );
2050 findLocks( aProjectPath );
2051 return aLockedFiles.empty();
2058bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
2060 bool extractSuccess =
true;
2062 std::function<void( git_tree*,
const wxString& )> extractTree =
2063 [&]( git_tree* t,
const wxString& prefix )
2065 if( !extractSuccess )
2068 size_t cnt = git_tree_entrycount( t );
2069 for(
size_t i = 0; i < cnt; ++i )
2071 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
2072 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
2073 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
2075 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
2077 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
2079 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
2082 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
2083 dirPath.GetPath() );
2084 extractSuccess =
false;
2088 git_tree* sub =
nullptr;
2089 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
2091 extractTree( sub, fullPath );
2092 git_tree_free( sub );
2095 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
2097 git_blob* blob =
nullptr;
2098 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
2100 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
2102 wxFileName dstDir( dst );
2103 dstDir.SetFullName( wxEmptyString );
2104 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
2106 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
2109 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
2115 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
2116 dst.GetFullPath() );
2117 extractSuccess =
false;
2118 git_blob_free( blob );
2122 git_blob_free( blob );
2128 extractTree( aTree, wxEmptyString );
2129 return extractSuccess;
2136void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
2137 std::set<wxString>& aFiles )
2139 wxDir dir( aSearchPath );
2140 if( !dir.IsOpened() )
2144 bool cont = dir.GetFirst( &filename );
2148 wxFileName fullPath( aSearchPath, filename );
2149 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
2151 if( fullPath.IsDir() && fullPath.DirExists() )
2153 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
2155 else if( fullPath.FileExists() )
2157 aFiles.insert( relativePath );
2160 cont = dir.GetNext( &filename );
2168bool shouldExcludeFromBackup(
const wxString& aFilename )
2175bool isPathUnderNestedProject(
const wxString& aProjectPath,
const wxString& aRelativePath )
2177 if( aRelativePath.IsEmpty() )
2180 wxArrayString parts = wxSplit( aRelativePath,
'/',
'\0' );
2182 if( parts.GetCount() < 2 )
2185 wxString accumulated = aProjectPath;
2189 for(
size_t i = 0; i + 1 < parts.GetCount(); ++i )
2191 accumulated += wxFileName::GetPathSeparator() + parts[i];
2204void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
2205 std::vector<wxString>& aFilesToDelete )
2207 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
2208 [&](
const wxString& dirPath,
const wxString& relativeBase )
2210 wxDir dir( dirPath );
2211 if( !dir.IsOpened() )
2215 bool cont = dir.GetFirst( &filename );
2223 cont = dir.GetNext( &filename );
2227 wxFileName fullPath( dirPath, filename );
2228 wxString relativePath = relativeBase.IsEmpty() ? filename :
2229 relativeBase + wxS(
"/") + filename;
2231 if( fullPath.IsDir() && fullPath.DirExists() )
2238 wxS(
"[history] findFilesToDelete: Skipping nested project "
2240 fullPath.GetFullPath() );
2244 scanDirectory( fullPath.GetFullPath(), relativePath );
2247 else if( fullPath.FileExists() )
2250 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
2253 if( !shouldExcludeFromBackup( filename ) )
2254 aFilesToDelete.push_back( relativePath );
2258 cont = dir.GetNext( &filename );
2262 scanDirectory( aProjectPath, wxEmptyString );
2270bool confirmFileDeletion( wxWindow* aParent,
const wxString& aProjectPath,
2271 const wxString& aBackupPath,
2272 const std::vector<wxString>& aFilesToDelete,
bool& aKeepAllFiles )
2274 if( aFilesToDelete.empty() || !aParent )
2276 aKeepAllFiles =
true;
2280 bool hasNestedProjectFile =
false;
2282 for(
const wxString& rel : aFilesToDelete )
2284 if( isPathUnderNestedProject( aProjectPath, rel ) )
2286 hasNestedProjectFile =
true;
2291 if( hasNestedProjectFile )
2293 wxLogTrace(
traceAutoSave, wxS(
"[history] Forcing keepAllFiles due to nested project under "
2294 "candidate path" ) );
2295 aKeepAllFiles =
true;
2299 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
2302 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
2303 for(
size_t i = 0; i < displayCount; ++i )
2305 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
2308 if( aFilesToDelete.size() > displayCount )
2310 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
2311 aFilesToDelete.size() - displayCount );
2315 wxYES_NO | wxCANCEL | wxNO_DEFAULT | wxICON_QUESTION );
2316 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
2317 dlg.SetExtendedMessage(
2318 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
2319 "files in the project directory. Choosing 'Proceed' will delete files that are not "
2320 "present in the restored commit." )
2322 + wxString::Format(
_(
"Files removed by 'Proceed' are archived to %s and can be "
2323 "recovered manually." ),
2326 int choice = dlg.ShowModal();
2328 if( choice == wxID_CANCEL )
2330 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
2333 else if( choice == wxID_NO )
2335 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
2336 aKeepAllFiles =
true;
2340 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
2341 aKeepAllFiles =
false;
2351bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
2352 const wxString& aTempRestorePath,
bool aKeepAllFiles,
2353 std::set<wxString>& aBackedUpFiles )
2355 wxDir currentDir( aProjectPath );
2356 if( !currentDir.IsOpened() )
2360 bool cont = currentDir.GetFirst( &filename );
2369 bool shouldBackup = !aKeepAllFiles;
2374 wxFileName testPath( aTempRestorePath, filename );
2375 shouldBackup = testPath.Exists();
2380 wxFileName source( aProjectPath, filename );
2381 wxFileName dest( aBackupPath, filename );
2384 if( !wxDirExists( aBackupPath ) )
2387 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
2389 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
2393 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
2394 source.GetFullPath(), dest.GetFullPath() );
2396 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
2399 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
2400 source.GetFullPath() );
2404 aBackedUpFiles.insert( filename );
2407 cont = currentDir.GetNext( &filename );
2417bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
2418 std::set<wxString>& aRestoredFiles )
2420 wxDir tempDir( aTempRestorePath );
2421 if( !tempDir.IsOpened() )
2425 bool cont = tempDir.GetFirst( &filename );
2429 wxFileName source( aTempRestorePath, filename );
2430 wxFileName dest( aProjectPath, filename );
2433 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
2434 source.GetFullPath(), dest.GetFullPath() );
2436 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
2439 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
2440 source.GetFullPath() );
2444 aRestoredFiles.insert( filename );
2445 cont = tempDir.GetNext( &filename );
2455void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
2456 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
2457 const std::set<wxString>& aRestoredFiles )
2459 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
2463 for(
const wxString& filename : aRestoredFiles )
2465 wxFileName toRemove( aProjectPath, filename );
2466 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
2467 toRemove.GetFullPath() );
2469 if( toRemove.DirExists() )
2471 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
2473 else if( toRemove.FileExists() )
2475 wxRemoveFile( toRemove.GetFullPath() );
2480 if( wxDirExists( aBackupPath ) )
2482 for(
const wxString& filename : aBackedUpFiles )
2484 wxFileName source( aBackupPath, filename );
2485 wxFileName dest( aProjectPath, filename );
2487 if( source.Exists() )
2489 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
2490 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
2491 dest.GetFullPath() );
2497 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
2498 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
2505bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
2506 const wxString& aHash )
2508 git_time_t t = git_commit_time( aCommit );
2509 wxDateTime dt( (time_t) t );
2510 git_signature* sig =
nullptr;
2512 git_commit* parent =
nullptr;
2515 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
2516 git_commit_lookup( &parent, aRepo, &parent_id );
2519 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
2522 const git_commit* constParent = parent;
2523 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
2524 msg.mb_str().data(), aTree, parent ? 1 : 0,
2525 parent ? &constParent :
nullptr );
2528 git_commit_free( parent );
2529 git_signature_free( sig );
2541 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
2544 std::vector<wxString> lockedFiles;
2545 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
2548 for(
const auto& f : lockedFiles )
2549 lockList += wxS(
"\n - ") + f;
2552 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
2558 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
2560 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
2571 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
2582 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
2584 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
2588 git_commit* commit =
nullptr;
2589 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2591 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
2595 git_tree* tree =
nullptr;
2596 git_commit_tree( &tree, commit );
2599 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
2601 std::vector<wxString> backupFiles;
2604 if( !backupFiles.empty() )
2608 backupFiles, wxS(
"Pre-restore backup" ) );
2613 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
2614 git_tree_free( tree );
2615 git_commit_free( commit );
2621 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Current state already matches HEAD; "
2622 "continuing without a new backup commit" ) );
2627 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
2629 if( wxDirExists( tempRestorePath ) )
2630 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2632 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
2635 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
2637 git_tree_free( tree );
2638 git_commit_free( commit );
2642 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
2645 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
2647 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
2648 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2649 git_tree_free( tree );
2650 git_commit_free( commit );
2655 std::set<wxString> restoredFiles;
2656 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
2658 std::vector<wxString> filesToDelete;
2659 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
2666 wxString backupPath =
2667 aProjectPath + wxS(
"_restore_backup_" )
2668 + wxDateTime::UNow().Format( wxS(
"%Y-%m-%dT%H-%M-%S-%l" ) );
2670 bool keepAllFiles =
true;
2671 if( !confirmFileDeletion( aParent, aProjectPath, backupPath, filesToDelete, keepAllFiles ) )
2674 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2675 git_tree_free( tree );
2676 git_commit_free( commit );
2681 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
2684 std::set<wxString> backedUpFiles;
2685 std::set<wxString> restoredFilesSet;
2688 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
2691 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2693 git_tree_free( tree );
2694 git_commit_free( commit );
2699 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
2701 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2703 git_tree_free( tree );
2704 git_commit_free( commit );
2710 wxS(
"[history] RestoreCommit: Restore successful, backup retained at %s" ),
2712 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2715 recordRestoreInHistory( repo, commit, tree, aHash );
2717 git_tree_free( tree );
2718 git_commit_free( commit );
2720 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
2729 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots =
LoadSnapshots( aProjectPath );
2731 if( snapshots.empty() )
2740 if( !selectedHash.IsEmpty() )
2747 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2750 git_repository* repo =
nullptr;
2752 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2755 git_revwalk* walk =
nullptr;
2756 if( git_revwalk_new( &walk, repo ) != 0 )
2758 git_repository_free( repo );
2762 git_revwalk_sorting( walk, GIT_SORT_TIME );
2763 git_revwalk_push_head( walk );
2767 while( git_revwalk_next( &oid, walk ) == 0 )
2769 git_commit* commit =
nullptr;
2771 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2775 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2776 info.date = wxDateTime(
static_cast<time_t
>( git_commit_time( commit ) ) );
2777 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2779 wxString firstLine =
info.message.BeforeFirst(
'\n' );
2781 long parsedCount = 0;
2783 firstLine.BeforeFirst(
':', &remainder );
2784 remainder.Trim(
true ).Trim(
false );
2786 if( remainder.EndsWith( wxS(
"files changed" ) ) )
2788 wxString countText = remainder.BeforeFirst(
' ' );
2790 if( countText.ToLong( &parsedCount ) )
2791 info.filesChanged =
static_cast<int>( parsedCount );
2794 info.summary = firstLine.BeforeFirst(
':' );
2797 info.message.BeforeFirst(
'\n', &rest );
2798 wxArrayString lines = wxSplit( rest,
'\n',
'\0' );
2800 for(
const wxString& line : lines )
2802 if( !line.IsEmpty() )
2803 info.changedFiles.Add( line );
2806 snapshots.push_back( std::move(
info ) );
2807 git_commit_free( commit );
2810 git_revwalk_free( walk );
2811 git_repository_free( repo );
2823 const wxString& aExtension )
2826 git_repository* repo =
nullptr;
2828 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2829 return wxEmptyString;
2832 git_commit* commit =
nullptr;
2833 git_tree* tree =
nullptr;
2835 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2837 git_repository_free( repo );
2838 return wxEmptyString;
2841 if( git_commit_tree( &tree, commit ) != 0 )
2843 git_commit_free( commit );
2844 git_repository_free( repo );
2845 return wxEmptyString;
2851 std::vector<wxString> entries;
2852 } ctx{ aExtension, {} };
2854 auto collect = [](
const char* aRoot,
const git_tree_entry* aEntry,
void* aPayload ) ->
int
2856 WALK_CTX* c =
static_cast<WALK_CTX*
>( aPayload );
2858 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2861 wxString
name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2863 if( !
name.EndsWith( c->ext ) )
2866 wxString
path = wxString::FromUTF8( aRoot ) +
name;
2867 c->entries.push_back(
path + wxS(
":" )
2868 + wxString::FromUTF8( git_oid_tostr_s( git_tree_entry_id( aEntry ) ) ) );
2872 git_tree_walk( tree, GIT_TREEWALK_PRE, collect, &ctx );
2874 git_tree_free( tree );
2875 git_commit_free( commit );
2876 git_repository_free( repo );
2878 std::sort( ctx.entries.begin(), ctx.entries.end() );
2880 wxString fingerprint;
2882 for(
const wxString& entry : ctx.entries )
2883 fingerprint << entry << wxS(
"|" );
2890 const wxString& aDestDir,
const std::vector<wxString>& aExtensions )
2893 git_repository* repo =
nullptr;
2895 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2899 git_commit* commit =
nullptr;
2900 git_tree* tree =
nullptr;
2902 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2904 git_repository_free( repo );
2908 if( git_commit_tree( &tree, commit ) != 0 )
2910 git_commit_free( commit );
2911 git_repository_free( repo );
2917 git_repository* repo;
2919 const std::vector<wxString>* extensions;
2921 } ctx{ repo, aDestDir, &aExtensions,
true };
2924 auto writeEntry = [](
const char* aRoot,
const git_tree_entry* aEntry,
void* aPayload ) ->
int
2926 WALK_CTX* c =
static_cast<WALK_CTX*
>( aPayload );
2928 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2931 wxString
name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2933 if( !c->extensions->empty() )
2937 for(
const wxString& ext : *c->extensions )
2939 if(
name.EndsWith( ext ) )
2950 wxString rel = wxString::FromUTF8( aRoot ) +
name;
2951 wxFileName outFn( c->destDir + wxS(
"/" ) + rel );
2953 if( !wxFileName::Mkdir( outFn.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
2959 git_blob* blob =
nullptr;
2961 if( git_blob_lookup( &blob, c->repo, git_tree_entry_id( aEntry ) ) == 0 )
2963 const void* data = git_blob_rawcontent( blob );
2964 const size_t size =
static_cast<size_t>( git_blob_rawsize( blob ) );
2965 wxFFile out( outFn.GetFullPath(), wxS(
"wb" ) );
2967 if( !( data && out.IsOpened() && out.Write( data, size ) == size ) )
2970 git_blob_free( blob );
2980 git_tree_walk( tree, GIT_TREEWALK_PRE, writeEntry, &ctx );
2982 git_tree_free( tree );
2983 git_commit_free( commit );
2984 git_repository_free( repo );
wxString GetSelectedHash() const
Hybrid locking mechanism for local history git repositories.
git_repository * GetRepository()
Get the git repository handle (only valid if IsLocked() returns true).
void ReleaseRepository()
Release git repository and index handles early, but keep the file lock.
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.
std::vector< LOCAL_HISTORY_SNAPSHOT_INFO > LoadSnapshots(const wxString &aProjectPath)
bool EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes, PROGRESS_REPORTER *aReporter=nullptr)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
bool TagSave(const wxString &aProjectPath, const wxString &aFileType)
Tag a manual save in the local history repository.
bool RunRegisteredSaversAndCommit(const wxString &aProjectPath, const wxString &aTitle, const wxString &aTagFileType=wxEmptyString)
Run all registered savers and, if any staged changes differ from HEAD, create a commit.
std::vector< std::pair< wxString, wxString > > FindStaleAutosaveFiles(const wxString &aProjectPath, const std::vector< wxString > &aExtensions) const
Enumerate autosave files newer than their corresponding source files for the project at aProjectPath,...
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.
bool commitInBackground(const wxString &aProjectPath, const wxString &aTitle, const std::vector< HISTORY_FILE_DATA > &aFileData, bool aIsManualSave)
Execute file writes and git commit on a background thread.
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
std::map< const void *, std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> > m_savers
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...
void WaitForPendingSave()
Block until any pending background save completes.
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> &aSaver)
Register a saver callback invoked during autosave history commits.
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::atomic< bool > m_saveInProgress
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 RunRegisteredSaversAsAutosaveFiles(const wxString &aProjectPath)
Run all registered savers and write their output to autosave files instead of committing to the local...
bool CommitFullProjectSnapshot(const wxString &aProjectPath, const wxString &aTitle)
Commit a snapshot of the entire project directory (excluding the .history directory and ignored trans...
std::vector< LOCAL_HISTORY_SNAPSHOT_INFO > GetSnapshots(const wxString &aProjectPath)
Snapshots (commits) for the project, newest first.
wxString TreeFingerprint(const wxString &aProjectPath, const wxString &aHash, const wxString &aExtension)
Fingerprint of all files ending in aExtension recorded by commit aHash (sorted path:blob pairs).
bool ExtractAllFilesAtCommit(const wxString &aProjectPath, const wxString &aHash, const wxString &aDestDir, const std::vector< wxString > &aExtensions={})
Write files recorded at aHash into aDestDir, recreating the project's relative folder structure.
std::future< bool > m_pendingFuture
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
void RemoveAutosaveFiles(const wxString &aProjectPath) const
Remove every autosave file under the project at aProjectPath regardless of which source it shadowed.
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
virtual COMMON_SETTINGS * GetCommonSettings() const
virtual SETTINGS_MANAGER & GetSettingsManager() const
A progress reporter interface for use in multi-threaded environments.
virtual void Report(const wxString &aMessage)=0
Display aMessage in the progress bar dialog.
virtual void AdvancePhase()=0
Use the next available virtual zone of the dialog progress bar.
virtual void SetCurrentProgress(double aProgress)=0
Set the progress value to aProgress (0..1).
COMMON_SETTINGS * GetCommonSettings() const
Retrieve the common settings shared by all applications.
wxString GetAutosaveRootForProject(const PROJECT *aProject=nullptr) const
Resolve the autosave-files root for a project.
PROJECT * GetProjectForPath(const wxString &aProjectPath) const
Return the active project iff its path matches aProjectPath, else nullptr.
wxString GetLocalHistoryDirForPath(const wxString &aProjectPath) const
Resolve the local-history directory for a project given by its on-disk path.
@ INCREMENTAL
Git-based local history (default)
@ PROJECT_DIR
Inside the project directory (default)
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
static const std::string LockFileExtension
static const std::string ProjectFileExtension
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 bool compactRepository(git_repository *aRepo, PROGRESS_REPORTER *aReporter=nullptr)
static bool isRestoreProtectedEntry(const wxString &aName)
static std::vector< std::pair< wxString, wxString > > findAutosaveFilePairs(const wxString &aProjectPath)
static const wxString AUTOSAVE_PREFIX
static bool commitSnapshotForProject(const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
static size_t dirSizeRecursive(const wxString &path)
static bool copyTreeObjects(git_repository *aSrcRepo, git_odb *aSrcOdb, git_odb *aDstOdb, const git_oid *aTreeOid, std::set< git_oid, bool(*)(const git_oid &, const git_oid &)> &aCopied)
static bool isKiCadProjectFile(const wxFileName &aFile)
static wxString sourceForAutosaveFile(const wxString &aAutosavePath, const wxString &aProjectPath, const wxString &aAutosaveRoot, BACKUP_LOCATION aLocation)
static wxString resolveAutosaveDestination(const wxString &aAutosaveRoot, const wxString &aRelativePath, BACKUP_LOCATION aLocation)
static SNAPSHOT_COMMIT_RESULT commitSnapshotWithLock(git_repository *repo, git_index *index, const wxString &aHistoryPath, const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
static bool filesContentEqual(const wxString &aPathA, const wxString &aPathB)
static bool formatUsesIncrementalHistory()
static bool isProjectDirectory(const wxString &aProjectPath)
static void collectProjectFiles(const wxString &aProjectPath, std::vector< wxString > &aFiles)
static wxString joinHistoryDestination(const wxString &aHistoryRoot, const wxString &aRelativePath)
PGM_BASE & Pgm()
The global program "get" accessor.
#define PROJECT_BACKUPS_DIR_SUFFIX
Project settings path will be <projectname> + this.
BACKUP_LOCATION location
Where backups, history, and autosave files live.
BACKUP_FORMAT format
Backup format (incremental git history vs zip archives)
Data produced by a registered saver on the UI thread, consumed by either the background local-history...
IbisParser parser & reporter
wxString result
Test unit parsing edge cases and error handling.
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
wxLogTrace helper definitions.
Definition of file extensions used in Kicad.