32#include <wx/filename.h>
36#include <wx/datetime.h>
39#include <wx/choicdlg.h>
52 wxFileName p( aProjectPath, wxEmptyString );
53 p.AppendDir( wxS(
".history" ) );
67 wxFileName fn( aFile );
69 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || fn.GetExt() == wxS(
"kicad_prl" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
77 const std::function<
void(
const wxString&, std::vector<wxString>& )>& aSaver )
81 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping"), aSaverObject );
86 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
92 auto it =
m_savers.find( aSaverObject );
97 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
105 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers") );
111 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
113 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
117 wxLogTrace(
traceAutoSave, wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu"),
118 aProjectPath, aTitle,
m_savers.size() );
122 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
126 std::vector<wxString> files;
128 for(
const auto& [saverObject, saver] :
m_savers )
130 size_t before = files.size();
131 saver( aProjectPath, files );
132 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p added %zu files (total=%zu)"),
133 saverObject, files.size() - before, files.size() );
137 wxString projectDir = aProjectPath;
138 if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
139 projectDir += wxFileName::GetPathSeparator();
141 auto it = std::remove_if( files.begin(), files.end(),
142 [&projectDir](
const wxString& file )
144 if( !file.StartsWith( projectDir ) )
146 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out file outside project: %s"), file );
151 files.erase( it, files.end() );
155 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no files; skipping") );
162 if( !lock.IsLocked() )
164 wxLogTrace(
traceAutoSave, wxS(
"[history] failed to acquire lock: %s"), lock.GetLockError() );
168 git_repository* repo = lock.GetRepository();
169 git_index* index = lock.GetIndex();
173 for(
const wxString& file : files )
175 wxFileName src( file );
177 if( !src.FileExists() )
179 wxLogTrace(
traceAutoSave, wxS(
"[history] skip missing '%s'"), file );
184 if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
186 std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
187 git_index_add_bypath( index, relHist.c_str() );
188 wxLogTrace(
traceAutoSave, wxS(
"[history] staged pre-mirrored '%s'"), file );
193 wxString proj = wxFileName( aProjectPath ).GetFullPath();
195 if( src.GetFullPath().StartsWith( proj + wxFILE_SEP_PATH ) )
196 relStr = src.GetFullPath().Mid( proj.length() + 1 );
198 relStr = src.GetFullName();
200 wxFileName dst( hist + wxFILE_SEP_PATH + relStr );
201 wxFileName dstDir( dst );
202 dstDir.SetFullName( wxEmptyString );
203 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
204 wxCopyFile( src.GetFullPath(), dst.GetFullPath(),
true );
205 std::string rel = dst.GetFullPath().ToStdString().substr( hist.length() + 1 );
206 git_index_add_bypath( index, rel.c_str() );
207 wxLogTrace(
traceAutoSave, wxS(
"[history] staged '%s' as '%s'"), file, wxString::FromUTF8( rel ) );
212 git_commit* head_commit =
nullptr;
213 git_tree* head_tree =
nullptr;
215 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
216 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
217 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
219 git_tree* rawIndexTree =
nullptr;
220 git_oid index_tree_oid;
222 if( git_index_write_tree( &index_tree_oid, index ) != 0 )
224 if( head_tree ) git_tree_free( head_tree );
225 if( head_commit ) git_commit_free( head_commit );
226 wxLogTrace(
traceAutoSave, wxS(
"[history] failed to write index tree" ) );
230 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
231 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
233 bool hasChanges =
true;
237 git_diff* diff =
nullptr;
239 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
241 hasChanges = git_diff_num_deltas( diff ) > 0;
242 wxLogTrace(
traceAutoSave, wxS(
"[history] diff deltas=%u"), (
unsigned) git_diff_num_deltas( diff ) );
243 git_diff_free( diff );
247 if( head_tree ) git_tree_free( head_tree );
248 if( head_commit ) git_commit_free( head_commit );
252 wxLogTrace(
traceAutoSave, wxS(
"[history] no changes detected; no commit") );
256 git_signature* rawSig =
nullptr;
258 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
260 git_commit* parent =
nullptr;
264 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
266 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
270 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
272 const git_commit* constParent = parent;
274 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
275 msg.mb_str().data(), indexTree.get(), parents,
276 parents ? &constParent :
nullptr );
279 wxLogTrace(
traceAutoSave, wxS(
"[history] commit created %s (%s files=%zu)"),
280 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, files.size() );
282 wxLogTrace(
traceAutoSave, wxS(
"[history] commit failed rc=%d"), rc );
284 if( parent ) git_commit_free( parent );
286 git_index_write( index );
301 if( aProjectPath.IsEmpty() )
304 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
309 if( !wxDirExists( hist ) )
312 git_repository* rawRepo =
nullptr;
313 bool isNewRepo =
false;
315 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
317 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
322 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
323 if( !ignoreFile.FileExists() )
325 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
328 f.Write( wxS(
"fp-info-cache\n*.kicad_prl\n*-backups\n" ) );
334 git_repository_free( rawRepo );
340 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: New repository created, collecting existing files" ) );
344 std::function<void(
const wxString& )> collect = [&](
const wxString&
path )
352 bool cont = d.GetFirst( &
name );
357 if(
name.StartsWith( wxS(
"." ) ) ||
name.EndsWith( wxS(
"-backups" ) ) )
359 cont = d.GetNext( &
name );
367 collect( fn.GetFullPath() );
369 else if( fn.FileExists() )
372 if( fn.GetFullName() != wxS(
"fp-info-cache" ) && fn.GetExt() != wxS(
"kicad_prl" ) )
373 files.Add( fn.GetFullPath() );
376 cont = d.GetNext( &
name );
380 collect( aProjectPath );
382 if( files.GetCount() > 0 )
384 std::vector<wxString> vec;
385 vec.reserve( files.GetCount() );
387 for(
unsigned i = 0; i < files.GetCount(); ++i )
388 vec.push_back( files[i] );
390 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: Creating initial snapshot with %zu files" ), vec.size() );
395 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: No files found to add to initial snapshot" ) );
405 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled )
408 wxString proj = wxFileName( aFiles[0] ).GetPath();
416 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitSnapshot failed to acquire lock: %s"),
424 for(
const wxString& file : aFiles )
426 wxFileName src( file );
429 if( src.GetFullPath().StartsWith( proj + wxFILE_SEP_PATH ) )
430 relStr = src.GetFullPath().Mid( proj.length() + 1 );
432 relStr = src.GetFullName();
434 wxFileName dst( hist + wxFILE_SEP_PATH + relStr );
437 wxFileName dstDir( dst );
438 dstDir.SetFullName( wxEmptyString );
439 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
442 wxCopyFile( src.GetFullPath(), dst.GetFullPath(),
true );
445 std::string rel = dst.GetFullPath().ToStdString().substr( hist.length() + 1 );
446 git_index_add_bypath( index, rel.c_str() );
450 if( git_index_write_tree( &tree_id, index ) != 0 )
453 git_tree* rawTree =
nullptr;
454 git_tree_lookup( &rawTree, repo, &tree_id );
455 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
457 git_signature* rawSig =
nullptr;
459 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
460 &git_signature_free );
462 git_commit* rawParent =
nullptr;
466 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
468 git_commit_lookup( &rawParent, repo, &parent_id );
472 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
475 git_tree* rawParentTree =
nullptr;
478 git_commit_tree( &rawParentTree, parent.get() );
480 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
482 git_diff* rawDiff =
nullptr;
483 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(), index,
nullptr );
484 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
488 if( !aTitle.IsEmpty() )
489 msg << aTitle << wxS(
": " );
491 msg << aFiles.size() << wxS(
" files changed" );
493 for(
size_t i = 0; i < git_diff_num_deltas( diff.get() ); ++i )
495 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
496 git_patch* rawPatch =
nullptr;
497 git_patch_from_diff( &rawPatch, diff.get(), i );
498 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
500 size_t context = 0, adds = 0, dels = 0;
501 git_patch_line_stats( &context, &adds, &dels, patch.get() );
502 size_t updated = std::min( adds, dels );
505 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
506 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
510 git_commit* parentPtr = parent.get();
511 const git_commit* constParentPtr = parentPtr;
512 git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
513 msg.mb_str().data(), tree.get(), parents,
514 parentPtr ? &constParentPtr :
nullptr );
515 git_index_write( index );
524 wxDir dir( aProjectPath );
526 if( !dir.IsOpened() )
530 std::function<void(
const wxString&)> collect = [&](
const wxString&
path )
538 bool cont = d.GetFirst( &
name );
542 if(
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) )
544 cont = d.GetNext( &
name );
550 if( fn.IsDir() && fn.DirExists() )
552 collect( fn.GetFullPath() );
554 else if( fn.FileExists() )
557 if( fn.GetFullName() != wxS(
"fp-info-cache" ) && fn.GetExt() != wxS(
"kicad_prl" ) )
558 files.Add( fn.GetFullPath() );
561 cont = d.GetNext( &
name );
565 collect( aProjectPath );
567 std::vector<wxString> vec;
568 vec.reserve( files.GetCount() );
570 for(
unsigned i = 0; i < files.GetCount(); ++i )
571 vec.push_back( files[i] );
587 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
597 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
602 git_reference* ref =
nullptr;
605 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
606 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
609 git_object* head_obj =
nullptr;
610 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
611 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
612 git_object_free( head_obj );
615 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
616 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
618 git_reference_delete( ref );
619 git_reference_free( ref );
622 git_oid last_tag_oid;
623 git_object* head_obj2 =
nullptr;
624 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
625 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
626 git_object_free( head_obj2 );
634 git_repository* repo =
nullptr;
636 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
640 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
642 git_repository_free( repo );
646 git_commit* head_commit =
nullptr;
647 git_commit_lookup( &head_commit, repo, &head_oid );
648 git_time_t head_time = git_commit_time( head_commit );
651 git_tag_list_match( &tags,
"Last_Save_*", repo );
652 git_time_t save_time = 0;
654 for(
size_t i = 0; i < tags.count; ++i )
656 git_reference* ref =
nullptr;
657 if( git_reference_lookup( &ref, repo,
658 ( wxS(
"refs/tags/" ) +
659 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
661 const git_oid* oid = git_reference_target( ref );
662 git_commit* c =
nullptr;
663 if( git_commit_lookup( &c, repo, oid ) == 0 )
665 git_time_t t = git_commit_time( c );
668 git_commit_free( c );
670 git_reference_free( ref );
674 git_strarray_free( &tags );
675 git_commit_free( head_commit );
676 git_repository_free( repo );
678 return save_time && head_time > save_time;
682 const wxString& aMessage )
688 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
697 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
698 git_reference* lastRef =
nullptr;
699 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
701 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
703 const git_oid* lastOid = git_reference_target( lastRef );
704 git_commit* lastCommit =
nullptr;
705 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
707 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
709 git_tree* lastTree =
nullptr;
710 git_commit_tree( &lastTree, lastCommit );
711 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
715 git_commit* headCommit =
nullptr;
717 const git_commit* parentArray[1];
718 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
719 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
721 parentArray[0] = headCommit;
725 git_signature* sigRaw =
nullptr;
727 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
729 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
730 git_oid newCommitOid;
731 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
732 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
733 if( headCommit ) git_commit_free( headCommit );
738 git_reference* existing =
nullptr;
739 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
741 git_reference_delete( existing );
742 git_reference_free( existing );
744 git_object* newCommitObj =
nullptr;
745 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
747 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
748 git_object_free( newCommitObj );
757 if( !dir.IsOpened() )
760 bool cont = dir.GetFirst( &
name );
766 else if( fn.FileExists() )
767 total += (size_t) fn.GetSize().GetValue();
768 cont = dir.GetNext( &
name );
780 if( !wxDirExists( hist ) )
785 if( current <= aMaxBytes )
792 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
802 git_revwalk* walk =
nullptr;
803 git_revwalk_new( &walk, repo );
804 git_revwalk_sorting( walk, GIT_SORT_TIME );
805 git_revwalk_push_head( walk );
806 std::vector<git_oid> commits;
809 while( git_revwalk_next( &oid, walk ) == 0 )
810 commits.push_back( oid );
812 git_revwalk_free( walk );
814 if( commits.empty() )
818 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
819 [](
const git_oid& a,
const git_oid& b )
821 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
824 size_t keptBytes = 0;
825 std::vector<git_oid> keep;
827 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
830 size_t cnt = git_tree_entrycount( tree );
831 for(
size_t i = 0; i < cnt; ++i )
833 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
835 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
837 const git_oid* bid = git_tree_entry_id( entry );
839 if( seenBlobs.find( *bid ) == seenBlobs.end() )
841 git_blob* blob =
nullptr;
843 if( git_blob_lookup( &blob, repo, bid ) == 0 )
845 added += git_blob_rawsize( blob );
846 git_blob_free( blob );
849 seenBlobs.insert( *bid );
852 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
854 git_tree* sub =
nullptr;
856 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
858 added += accountTree( sub );
859 git_tree_free( sub );
866 for(
const git_oid& cOid : commits )
868 git_commit* c =
nullptr;
870 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
873 git_tree* tree =
nullptr;
874 git_commit_tree( &tree, c );
875 size_t add = accountTree( tree );
876 git_tree_free( tree );
877 git_commit_free( c );
879 if( keep.empty() || keptBytes + add <= aMaxBytes )
881 keep.push_back( cOid );
889 keep.push_back( commits.front() );
893 std::vector<std::pair<wxString, git_oid>> tagTargets;
894 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
895 [](
const git_oid& a,
const git_oid& b )
897 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
899 git_strarray tagList;
901 if( git_tag_list( &tagList, repo ) == 0 )
903 for(
size_t i = 0; i < tagList.count; ++i )
905 wxString
name = wxString::FromUTF8( tagList.strings[i] );
906 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
908 git_reference* tref =
nullptr;
910 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
912 const git_oid* toid = git_reference_target( tref );
916 tagTargets.emplace_back(
name, *toid );
917 taggedCommits.insert( *toid );
921 for(
const auto& k : keep )
923 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
933 keep.push_back( *toid );
934 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
939 git_reference_free( tref );
943 git_strarray_free( &tagList );
947 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
948 wxString trimPath = trimFn.GetPath();
950 if( wxDirExists( trimPath ) )
951 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
954 git_repository* newRepo =
nullptr;
956 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
960 std::reverse( keep.begin(), keep.end() );
961 git_commit* parent =
nullptr;
962 struct MAP_ENTRY { git_oid orig; git_oid neu; };
963 std::vector<MAP_ENTRY> commitMap;
965 for(
const git_oid& co : keep )
967 git_commit* orig =
nullptr;
969 if( git_commit_lookup( &orig, repo, &co ) != 0 )
972 git_tree* tree =
nullptr;
973 git_commit_tree( &tree, orig );
977 wxArrayString toDelete;
980 bool cont = d.GetFirst( &nm );
983 if( nm != wxS(
".git") )
985 cont = d.GetNext( &nm );
988 for(
auto& del : toDelete )
990 wxFileName f( trimPath, del );
992 wxFileName::Rmdir( f.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
993 else if( f.FileExists() )
994 wxRemoveFile( f.GetFullPath() );
998 std::function<void(git_tree*,
const wxString&)> writeTree = [&]( git_tree* t,
const wxString& base )
1000 size_t ecnt = git_tree_entrycount( t );
1001 for(
size_t i = 0; i < ecnt; ++i )
1003 const git_tree_entry* e = git_tree_entry_byindex( t, i );
1004 wxString
name = wxString::FromUTF8( git_tree_entry_name( e ) );
1006 if( git_tree_entry_type( e ) == GIT_OBJECT_TREE )
1008 wxFileName dir( base,
name );
1009 wxMkdir( dir.GetFullPath() );
1010 git_tree* sub =
nullptr;
1012 if( git_tree_lookup( &sub, repo, git_tree_entry_id( e ) ) == 0 )
1014 writeTree( sub, dir.GetFullPath() );
1015 git_tree_free( sub );
1018 else if( git_tree_entry_type( e ) == GIT_OBJECT_BLOB )
1020 git_blob* blob =
nullptr;
1022 if( git_blob_lookup( &blob, repo, git_tree_entry_id( e ) ) == 0 )
1024 wxFileName file( base,
name );
1025 wxFFile f( file.GetFullPath(), wxT(
"wb") );
1029 f.Write( (
const char*) git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1032 git_blob_free( blob );
1038 writeTree( tree, trimPath );
1040 git_index* newIndex =
nullptr;
1041 git_repository_index( &newIndex, newRepo );
1042 git_index_add_all( newIndex,
nullptr, 0,
nullptr,
nullptr );
1043 git_index_write( newIndex );
1045 git_index_write_tree( &newTreeOid, newIndex );
1046 git_tree* newTree =
nullptr;
1047 git_tree_lookup( &newTree, newRepo, &newTreeOid );
1050 const git_signature* origAuthor = git_commit_author( orig );
1051 const git_signature* origCommitter = git_commit_committer( orig );
1052 git_signature* sigAuthor =
nullptr;
1053 git_signature* sigCommitter =
nullptr;
1055 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1056 origAuthor->when.time, origAuthor->when.offset );
1057 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1058 origCommitter->when.time, origCommitter->when.offset );
1060 const git_commit* parents[1];
1061 int parentCount = 0;
1065 parents[0] = parent;
1069 git_oid newCommitOid;
1070 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1071 newTree, parentCount, parentCount ? parents :
nullptr );
1073 git_commit_free( parent );
1075 git_commit_lookup( &parent, newRepo, &newCommitOid );
1077 commitMap.emplace_back( co, newCommitOid );
1079 git_signature_free( sigAuthor );
1080 git_signature_free( sigCommitter );
1081 git_tree_free( newTree );
1082 git_index_free( newIndex );
1083 git_tree_free( tree );
1084 git_commit_free( orig );
1088 git_commit_free( parent );
1091 for(
const auto& tt : tagTargets )
1094 const git_oid* newOid =
nullptr;
1096 for(
const auto& m : commitMap )
1098 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1108 git_object* obj =
nullptr;
1110 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1112 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1113 git_object_free( obj );
1120 git_repository_free( newRepo );
1123 wxString backupOld = hist + wxS(
"_old");
1124 wxRenameFile( hist, backupOld );
1125 wxRenameFile( trimPath, hist );
1126 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1133 git_repository* repo =
nullptr;
1135 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1136 return wxEmptyString;
1139 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1141 git_repository_free( repo );
1142 return wxEmptyString;
1145 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1146 git_repository_free( repo );
1158bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1160 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
1162 wxDir dir( dirPath );
1163 if( !dir.IsOpened() )
1167 bool cont = dir.GetFirst( &filename );
1171 wxFileName fullPath( dirPath, filename );
1174 if( filename == wxS(
".history") || filename == wxS(
".git") )
1176 cont = dir.GetNext( &filename );
1180 if( fullPath.DirExists() )
1182 findLocks( fullPath.GetFullPath() );
1184 else if( fullPath.FileExists() && filename.EndsWith( wxS(
".lock") ) )
1187 LOCKFILE testLock( fullPath.GetFullPath().BeforeLast(
'.' ) );
1188 if( testLock.Valid() && !testLock.IsLockedByMe() )
1190 aLockedFiles.push_back( fullPath.GetFullPath() );
1194 cont = dir.GetNext( &filename );
1198 findLocks( aProjectPath );
1199 return aLockedFiles.empty();
1206bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
1208 bool extractSuccess =
true;
1210 std::function<void( git_tree*,
const wxString& )> extractTree =
1211 [&]( git_tree* t,
const wxString& prefix )
1213 if( !extractSuccess )
1216 size_t cnt = git_tree_entrycount( t );
1217 for(
size_t i = 0; i < cnt; ++i )
1219 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1220 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1221 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
1223 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1225 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
1227 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1230 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
1231 dirPath.GetPath() );
1232 extractSuccess =
false;
1236 git_tree* sub =
nullptr;
1237 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
1239 extractTree( sub, fullPath );
1240 git_tree_free( sub );
1243 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1245 git_blob* blob =
nullptr;
1246 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
1248 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
1250 wxFileName dstDir( dst );
1251 dstDir.SetFullName( wxEmptyString );
1252 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1254 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
1257 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1263 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
1264 dst.GetFullPath() );
1265 extractSuccess =
false;
1266 git_blob_free( blob );
1270 git_blob_free( blob );
1276 extractTree( aTree, wxEmptyString );
1277 return extractSuccess;
1284void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
1285 std::set<wxString>& aFiles )
1287 wxDir dir( aSearchPath );
1288 if( !dir.IsOpened() )
1292 bool cont = dir.GetFirst( &filename );
1296 wxFileName fullPath( aSearchPath, filename );
1297 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
1299 if( fullPath.IsDir() && fullPath.DirExists() )
1301 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
1303 else if( fullPath.FileExists() )
1305 aFiles.insert( relativePath );
1308 cont = dir.GetNext( &filename );
1316void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
1317 std::vector<wxString>& aFilesToDelete )
1319 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
1320 [&](
const wxString& dirPath,
const wxString& relativeBase )
1322 wxDir dir( dirPath );
1323 if( !dir.IsOpened() )
1327 bool cont = dir.GetFirst( &filename );
1332 if( filename == wxS(
".history") || filename == wxS(
".git") ||
1333 filename == wxS(
"_restore_backup") || filename == wxS(
"_restore_temp") )
1335 cont = dir.GetNext( &filename );
1339 wxFileName fullPath( dirPath, filename );
1340 wxString relativePath = relativeBase.IsEmpty() ? filename :
1341 relativeBase + wxS(
"/") + filename;
1343 if( fullPath.IsDir() && fullPath.DirExists() )
1345 scanDirectory( fullPath.GetFullPath(), relativePath );
1347 else if( fullPath.FileExists() )
1350 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
1352 aFilesToDelete.push_back( relativePath );
1356 cont = dir.GetNext( &filename );
1360 scanDirectory( aProjectPath, wxEmptyString );
1368bool confirmFileDeletion( wxWindow* aParent,
const std::vector<wxString>& aFilesToDelete,
1369 bool& aKeepAllFiles )
1371 if( aFilesToDelete.empty() || !aParent )
1373 aKeepAllFiles =
false;
1377 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
1380 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
1381 for(
size_t i = 0; i < displayCount; ++i )
1383 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
1386 if( aFilesToDelete.size() > displayCount )
1388 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
1389 aFilesToDelete.size() - displayCount );
1392 wxMessageDialog dlg( aParent, message,
_(
"Delete Files during Restore" ),
1393 wxYES_NO | wxCANCEL | wxICON_QUESTION );
1394 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
1395 dlg.SetExtendedMessage(
1396 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
1397 "files in the project directory. Choosing 'Proceed' will delete files that are not "
1398 "present in the restored commit." ) );
1400 int choice = dlg.ShowModal();
1402 if( choice == wxID_CANCEL )
1404 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
1407 else if( choice == wxID_NO )
1409 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
1410 aKeepAllFiles =
true;
1414 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
1415 aKeepAllFiles =
false;
1425bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
1426 const wxString& aTempRestorePath,
bool aKeepAllFiles,
1427 std::set<wxString>& aBackedUpFiles )
1429 wxDir currentDir( aProjectPath );
1430 if( !currentDir.IsOpened() )
1434 bool cont = currentDir.GetFirst( &filename );
1438 if( filename != wxS(
".history" ) && filename != wxS(
".git" ) &&
1439 filename != wxS(
"_restore_backup" ) && filename != wxS(
"_restore_temp" ) )
1442 bool shouldBackup = !aKeepAllFiles;
1447 wxFileName testPath( aTempRestorePath, filename );
1448 shouldBackup = testPath.Exists();
1453 wxFileName source( aProjectPath, filename );
1454 wxFileName dest( aBackupPath, filename );
1457 if( !wxDirExists( aBackupPath ) )
1460 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
1462 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
1466 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
1467 source.GetFullPath(), dest.GetFullPath() );
1469 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1472 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
1473 source.GetFullPath() );
1477 aBackedUpFiles.insert( filename );
1480 cont = currentDir.GetNext( &filename );
1490bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
1491 std::set<wxString>& aRestoredFiles )
1493 wxDir tempDir( aTempRestorePath );
1494 if( !tempDir.IsOpened() )
1498 bool cont = tempDir.GetFirst( &filename );
1502 wxFileName source( aTempRestorePath, filename );
1503 wxFileName dest( aProjectPath, filename );
1506 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
1507 source.GetFullPath(), dest.GetFullPath() );
1509 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1512 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
1513 source.GetFullPath() );
1517 aRestoredFiles.insert( filename );
1518 cont = tempDir.GetNext( &filename );
1528void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
1529 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
1530 const std::set<wxString>& aRestoredFiles )
1532 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
1536 for(
const wxString& filename : aRestoredFiles )
1538 wxFileName toRemove( aProjectPath, filename );
1539 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
1540 toRemove.GetFullPath() );
1542 if( toRemove.DirExists() )
1544 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1546 else if( toRemove.FileExists() )
1548 wxRemoveFile( toRemove.GetFullPath() );
1553 if( wxDirExists( aBackupPath ) )
1555 for(
const wxString& filename : aBackedUpFiles )
1557 wxFileName source( aBackupPath, filename );
1558 wxFileName dest( aProjectPath, filename );
1560 if( source.Exists() )
1562 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1563 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
1564 dest.GetFullPath() );
1570 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
1571 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
1578bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
1579 const wxString& aHash )
1581 git_time_t t = git_commit_time( aCommit );
1582 wxDateTime dt( (time_t) t );
1583 git_signature* sig =
nullptr;
1585 git_commit* parent =
nullptr;
1588 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
1589 git_commit_lookup( &parent, aRepo, &parent_id );
1592 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1595 const git_commit* constParent = parent;
1596 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
1597 msg.mb_str().data(), aTree, parent ? 1 : 0,
1598 parent ? &constParent :
nullptr );
1601 git_commit_free( parent );
1602 git_signature_free( sig );
1614 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
1617 std::vector<wxString> lockedFiles;
1618 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
1621 for(
const auto& f : lockedFiles )
1622 lockList += wxS(
"\n - ") + f;
1625 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
1636 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
1647 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
1649 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
1653 git_commit* commit =
nullptr;
1654 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
1656 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
1660 git_tree* tree =
nullptr;
1661 git_commit_tree( &tree, commit );
1664 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
1669 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
1670 git_tree_free( tree );
1671 git_commit_free( commit );
1676 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
1678 if( wxDirExists( tempRestorePath ) )
1679 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1681 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
1684 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
1686 git_tree_free( tree );
1687 git_commit_free( commit );
1691 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
1694 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
1696 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
1697 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1698 git_tree_free( tree );
1699 git_commit_free( commit );
1704 std::set<wxString> restoredFiles;
1705 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
1707 std::vector<wxString> filesToDelete;
1708 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
1710 bool keepAllFiles =
true;
1711 if( !confirmFileDeletion( aParent, filesToDelete, keepAllFiles ) )
1714 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1715 git_tree_free( tree );
1716 git_commit_free( commit );
1721 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
1723 wxString backupPath = aProjectPath + wxS(
"_restore_backup");
1726 if( wxDirExists( backupPath ) )
1728 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Removing old backup %s" ),
1730 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1734 std::set<wxString> backedUpFiles;
1735 std::set<wxString> restoredFilesSet;
1738 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
1741 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1743 git_tree_free( tree );
1744 git_commit_free( commit );
1749 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
1751 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1753 git_tree_free( tree );
1754 git_commit_free( commit );
1759 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Restore successful, cleaning up" ) );
1760 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1761 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1764 recordRestoreInHistory( repo, commit, tree, aHash );
1766 git_tree_free( tree );
1767 git_commit_free( commit );
1769 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
1779 git_repository* repo =
nullptr;
1781 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1784 git_revwalk* walk =
nullptr;
1785 git_revwalk_new( &walk, repo );
1786 git_revwalk_push_head( walk );
1788 std::vector<wxString> choices;
1789 std::vector<wxString> hashes;
1792 while( git_revwalk_next( &oid, walk ) == 0 )
1794 git_commit* commit =
nullptr;
1795 git_commit_lookup( &commit, repo, &oid );
1797 git_time_t t = git_commit_time( commit );
1798 wxDateTime dt( (time_t) t );
1801 line.Printf( wxS(
"%s %s" ), dt.FormatISOCombined().c_str(),
1802 wxString::FromUTF8( git_commit_summary( commit ) ) );
1803 choices.push_back( line );
1804 hashes.push_back( wxString::FromUTF8( git_oid_tostr_s( &oid ) ) );
1805 git_commit_free( commit );
1808 git_revwalk_free( walk );
1809 git_repository_free( repo );
1811 if( choices.empty() )
1814 int index = wxGetSingleChoiceIndex(
_(
"Select snapshot" ),
_(
"Restore" ),
1815 (
int) choices.size(), &choices[0], aParent );
1817 if( index != wxNOT_FOUND )
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.
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)
PGM_BASE & Pgm()
The global program "get" accessor.
wxString result
Test unit parsing edge cases and error handling.
wxLogTrace helper definitions.