33#include <wx/filename.h>
37#include <wx/datetime.h>
40#include <wx/choicdlg.h>
53 wxFileName p( aProjectPath, wxEmptyString );
54 p.AppendDir( wxS(
".history" ) );
68 wxFileName fn( aFile );
70 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
78 const std::function<
void(
const wxString&, std::vector<wxString>& )>& aSaver )
82 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping"), aSaverObject );
87 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
93 auto it =
m_savers.find( aSaverObject );
98 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
106 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers") );
112 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
114 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
118 wxLogTrace(
traceAutoSave, wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu"),
119 aProjectPath, aTitle,
m_savers.size() );
123 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
127 std::vector<wxString> files;
129 for(
const auto& [saverObject, saver] :
m_savers )
131 size_t before = files.size();
132 saver( aProjectPath, files );
133 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p added %zu files (total=%zu)"),
134 saverObject, files.size() - before, files.size() );
138 wxString projectDir = aProjectPath;
139 if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
140 projectDir += wxFileName::GetPathSeparator();
142 auto it = std::remove_if( files.begin(), files.end(),
143 [&projectDir](
const wxString& file )
145 if( !file.StartsWith( projectDir ) )
147 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out file outside project: %s"), file );
152 files.erase( it, files.end() );
156 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no files; skipping") );
163 if( !lock.IsLocked() )
165 wxLogTrace(
traceAutoSave, wxS(
"[history] failed to acquire lock: %s"), lock.GetLockError() );
169 git_repository* repo = lock.GetRepository();
170 git_index*
index = lock.GetIndex();
174 for(
const wxString& file : files )
176 wxFileName src( file );
178 if( !src.FileExists() )
180 wxLogTrace(
traceAutoSave, wxS(
"[history] skip missing '%s'"), file );
185 if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
187 std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
188 git_index_add_bypath(
index, relHist.c_str() );
189 wxLogTrace(
traceAutoSave, wxS(
"[history] staged pre-mirrored '%s'"), file );
194 wxString proj = wxFileName( aProjectPath ).GetFullPath();
196 if( src.GetFullPath().StartsWith( proj + wxFILE_SEP_PATH ) )
197 relStr = src.GetFullPath().Mid( proj.length() + 1 );
199 relStr = src.GetFullName();
201 wxFileName dst( hist + wxFILE_SEP_PATH + relStr );
202 wxFileName dstDir( dst );
203 dstDir.SetFullName( wxEmptyString );
204 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
205 wxCopyFile( src.GetFullPath(), dst.GetFullPath(),
true );
206 std::string rel = dst.GetFullPath().ToStdString().substr( hist.length() + 1 );
207 git_index_add_bypath(
index, rel.c_str() );
208 wxLogTrace(
traceAutoSave, wxS(
"[history] staged '%s' as '%s'"), file, wxString::FromUTF8( rel ) );
213 git_commit* head_commit =
nullptr;
214 git_tree* head_tree =
nullptr;
216 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
217 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
218 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
220 git_tree* rawIndexTree =
nullptr;
221 git_oid index_tree_oid;
223 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
225 if( head_tree ) git_tree_free( head_tree );
226 if( head_commit ) git_commit_free( head_commit );
227 wxLogTrace(
traceAutoSave, wxS(
"[history] failed to write index tree" ) );
231 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
232 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
234 bool hasChanges =
true;
238 git_diff* diff =
nullptr;
240 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
242 hasChanges = git_diff_num_deltas( diff ) > 0;
243 wxLogTrace(
traceAutoSave, wxS(
"[history] diff deltas=%u"), (
unsigned) git_diff_num_deltas( diff ) );
244 git_diff_free( diff );
248 if( head_tree ) git_tree_free( head_tree );
249 if( head_commit ) git_commit_free( head_commit );
253 wxLogTrace(
traceAutoSave, wxS(
"[history] no changes detected; no commit") );
257 git_signature* rawSig =
nullptr;
259 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
261 git_commit* parent =
nullptr;
265 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
267 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
271 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
273 const git_commit* constParent = parent;
275 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
276 msg.mb_str().data(), indexTree.get(), parents,
277 parents ? &constParent :
nullptr );
280 wxLogTrace(
traceAutoSave, wxS(
"[history] commit created %s (%s files=%zu)"),
281 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, files.size() );
283 wxLogTrace(
traceAutoSave, wxS(
"[history] commit failed rc=%d"), rc );
285 if( parent ) git_commit_free( parent );
287 git_index_write(
index );
302 if( aProjectPath.IsEmpty() )
305 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
310 if( !wxDirExists( hist ) )
312 if( wxIsWritable( aProjectPath ) )
314 if( !wxMkdir( hist ) )
321 git_repository* rawRepo =
nullptr;
322 bool isNewRepo =
false;
324 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
326 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
331 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
332 if( !ignoreFile.FileExists() )
334 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
337 f.Write( wxS(
"fp-info-cache\n*-backups\nREADME.txt\n" ) );
342 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
344 if( !readmeFile.FileExists() )
346 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
350 f.Write( wxS(
"KiCad Local History Directory\n"
351 "=============================\n\n"
352 "This directory contains automatic snapshots of your project files.\n"
353 "KiCad periodically saves copies of your work here, allowing you to\n"
354 "recover from accidental changes or data loss.\n\n"
355 "You can browse and restore previous versions through KiCad's\n"
356 "File > Local History menu.\n\n"
357 "To disable this feature:\n"
358 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
359 "This directory can be safely deleted if you no longer need the\n"
360 "history, but doing so will permanently remove all saved snapshots.\n" ) );
366 git_repository_free( rawRepo );
372 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: New repository created, collecting existing files" ) );
376 std::function<void(
const wxString& )> collect = [&](
const wxString&
path )
384 bool cont = d.GetFirst( &
name );
389 if(
name.StartsWith( wxS(
"." ) ) ||
name.EndsWith( wxS(
"-backups" ) ) )
391 cont = d.GetNext( &
name );
399 collect( fn.GetFullPath() );
401 else if( fn.FileExists() )
404 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
405 files.Add( fn.GetFullPath() );
408 cont = d.GetNext( &
name );
412 collect( aProjectPath );
414 if( files.GetCount() > 0 )
416 std::vector<wxString> vec;
417 vec.reserve( files.GetCount() );
419 for(
unsigned i = 0; i < files.GetCount(); ++i )
420 vec.push_back( files[i] );
422 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: Creating initial snapshot with %zu files" ), vec.size() );
427 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: No files found to add to initial snapshot" ) );
437 const wxString& aHistoryPath,
const wxString& aProjectPath,
438 const std::vector<wxString>& aFiles,
const wxString& aTitle )
440 for(
const wxString& file : aFiles )
442 wxFileName src( file );
445 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
446 relStr = src.GetFullPath().Mid( aProjectPath.length() + 1 );
448 relStr = src.GetFullName();
450 wxFileName dst( aHistoryPath + wxFILE_SEP_PATH + relStr );
453 wxFileName dstDir( dst );
454 dstDir.SetFullName( wxEmptyString );
455 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
458 wxCopyFile( src.GetFullPath(), dst.GetFullPath(),
true );
461 std::string rel = dst.GetFullPath().ToStdString().substr( aHistoryPath.length() + 1 );
462 git_index_add_bypath(
index, rel.c_str() );
466 if( git_index_write_tree( &tree_id,
index ) != 0 )
469 git_tree* rawTree =
nullptr;
470 git_tree_lookup( &rawTree, repo, &tree_id );
471 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
473 git_signature* rawSig =
nullptr;
475 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
476 &git_signature_free );
478 git_commit* rawParent =
nullptr;
482 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
484 git_commit_lookup( &rawParent, repo, &parent_id );
488 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
491 git_tree* rawParentTree =
nullptr;
494 git_commit_tree( &rawParentTree, parent.get() );
496 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
498 git_diff* rawDiff =
nullptr;
499 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
500 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
504 if( !aTitle.IsEmpty() )
505 msg << aTitle << wxS(
": " );
507 msg << aFiles.size() << wxS(
" files changed" );
509 for(
size_t i = 0; i < git_diff_num_deltas( diff.get() ); ++i )
511 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
512 git_patch* rawPatch =
nullptr;
513 git_patch_from_diff( &rawPatch, diff.get(), i );
514 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
516 size_t context = 0, adds = 0, dels = 0;
517 git_patch_line_stats( &context, &adds, &dels, patch.get() );
518 size_t updated = std::min( adds, dels );
521 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
522 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
526 git_commit* parentPtr = parent.get();
527 const git_commit* constParentPtr = parentPtr;
528 git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
529 msg.mb_str().data(), tree.get(), parents,
530 parentPtr ? &constParentPtr :
nullptr );
531 git_index_write(
index );
538 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled )
541 wxString proj = wxFileName( aFiles[0] ).GetPath();
549 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitSnapshot failed to acquire lock: %s"),
564 wxDir dir( aProjectPath );
566 if( !dir.IsOpened() )
570 std::function<void(
const wxString&)> collect = [&](
const wxString&
path )
578 bool cont = d.GetFirst( &
name );
582 if(
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) )
584 cont = d.GetNext( &
name );
590 if( fn.IsDir() && fn.DirExists() )
592 collect( fn.GetFullPath() );
594 else if( fn.FileExists() )
597 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
598 aFiles.push_back( fn.GetFullPath() );
601 cont = d.GetNext( &
name );
605 collect( aProjectPath );
611 std::vector<wxString> files;
631 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
641 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
646 git_reference* ref =
nullptr;
649 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
650 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
653 git_object* head_obj =
nullptr;
654 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
655 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
656 git_object_free( head_obj );
659 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
660 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
662 git_reference_delete( ref );
663 git_reference_free( ref );
666 git_oid last_tag_oid;
667 git_object* head_obj2 =
nullptr;
668 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
669 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
670 git_object_free( head_obj2 );
678 git_repository* repo =
nullptr;
680 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
684 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
686 git_repository_free( repo );
690 git_commit* head_commit =
nullptr;
691 git_commit_lookup( &head_commit, repo, &head_oid );
692 git_time_t head_time = git_commit_time( head_commit );
695 git_tag_list_match( &tags,
"Last_Save_*", repo );
696 git_time_t save_time = 0;
698 for(
size_t i = 0; i < tags.count; ++i )
700 git_reference* ref =
nullptr;
701 if( git_reference_lookup( &ref, repo,
702 ( wxS(
"refs/tags/" ) +
703 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
705 const git_oid* oid = git_reference_target( ref );
706 git_commit* c =
nullptr;
707 if( git_commit_lookup( &c, repo, oid ) == 0 )
709 git_time_t t = git_commit_time( c );
712 git_commit_free( c );
714 git_reference_free( ref );
718 git_strarray_free( &tags );
719 git_commit_free( head_commit );
720 git_repository_free( repo );
727 return head_time > save_time;
731 const wxString& aMessage )
737 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
746 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
747 git_reference* lastRef =
nullptr;
748 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
750 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
752 const git_oid* lastOid = git_reference_target( lastRef );
753 git_commit* lastCommit =
nullptr;
754 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
756 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
758 git_tree* lastTree =
nullptr;
759 git_commit_tree( &lastTree, lastCommit );
760 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
764 git_commit* headCommit =
nullptr;
766 const git_commit* parentArray[1];
767 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
768 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
770 parentArray[0] = headCommit;
774 git_signature* sigRaw =
nullptr;
776 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
778 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
779 git_oid newCommitOid;
780 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
781 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
782 if( headCommit ) git_commit_free( headCommit );
787 git_reference* existing =
nullptr;
788 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
790 git_reference_delete( existing );
791 git_reference_free( existing );
793 git_object* newCommitObj =
nullptr;
794 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
796 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
797 git_object_free( newCommitObj );
806 if( !dir.IsOpened() )
809 bool cont = dir.GetFirst( &
name );
815 else if( fn.FileExists() )
816 total += (size_t) fn.GetSize().GetValue();
817 cont = dir.GetNext( &
name );
829 if( !wxDirExists( hist ) )
834 if( current <= aMaxBytes )
841 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
851 git_revwalk* walk =
nullptr;
852 git_revwalk_new( &walk, repo );
853 git_revwalk_sorting( walk, GIT_SORT_TIME );
854 git_revwalk_push_head( walk );
855 std::vector<git_oid> commits;
858 while( git_revwalk_next( &oid, walk ) == 0 )
859 commits.push_back( oid );
861 git_revwalk_free( walk );
863 if( commits.empty() )
867 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
868 [](
const git_oid& a,
const git_oid& b )
870 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
873 size_t keptBytes = 0;
874 std::vector<git_oid> keep;
876 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
879 size_t cnt = git_tree_entrycount( tree );
880 for(
size_t i = 0; i < cnt; ++i )
882 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
884 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
886 const git_oid* bid = git_tree_entry_id( entry );
888 if( seenBlobs.find( *bid ) == seenBlobs.end() )
890 git_blob* blob =
nullptr;
892 if( git_blob_lookup( &blob, repo, bid ) == 0 )
894 added += git_blob_rawsize( blob );
895 git_blob_free( blob );
898 seenBlobs.insert( *bid );
901 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
903 git_tree* sub =
nullptr;
905 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
907 added += accountTree( sub );
908 git_tree_free( sub );
915 for(
const git_oid& cOid : commits )
917 git_commit* c =
nullptr;
919 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
922 git_tree* tree =
nullptr;
923 git_commit_tree( &tree, c );
924 size_t add = accountTree( tree );
925 git_tree_free( tree );
926 git_commit_free( c );
928 if( keep.empty() || keptBytes + add <= aMaxBytes )
930 keep.push_back( cOid );
938 keep.push_back( commits.front() );
942 std::vector<std::pair<wxString, git_oid>> tagTargets;
943 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
944 [](
const git_oid& a,
const git_oid& b )
946 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
948 git_strarray tagList;
950 if( git_tag_list( &tagList, repo ) == 0 )
952 for(
size_t i = 0; i < tagList.count; ++i )
954 wxString
name = wxString::FromUTF8( tagList.strings[i] );
955 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
957 git_reference* tref =
nullptr;
959 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
961 const git_oid* toid = git_reference_target( tref );
965 tagTargets.emplace_back(
name, *toid );
966 taggedCommits.insert( *toid );
970 for(
const auto& k : keep )
972 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
982 keep.push_back( *toid );
983 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
988 git_reference_free( tref );
992 git_strarray_free( &tagList );
996 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
997 wxString trimPath = trimFn.GetPath();
999 if( wxDirExists( trimPath ) )
1000 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1002 wxMkdir( trimPath );
1003 git_repository* newRepo =
nullptr;
1005 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1009 std::reverse( keep.begin(), keep.end() );
1010 git_commit* parent =
nullptr;
1011 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1012 std::vector<MAP_ENTRY> commitMap;
1014 for(
const git_oid& co : keep )
1016 git_commit* orig =
nullptr;
1018 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1021 git_tree* tree =
nullptr;
1022 git_commit_tree( &tree, orig );
1026 wxArrayString toDelete;
1027 wxDir d( trimPath );
1029 bool cont = d.GetFirst( &nm );
1032 if( nm != wxS(
".git") )
1034 cont = d.GetNext( &nm );
1037 for(
auto& del : toDelete )
1039 wxFileName f( trimPath, del );
1041 wxFileName::Rmdir( f.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1042 else if( f.FileExists() )
1043 wxRemoveFile( f.GetFullPath() );
1047 std::function<void(git_tree*,
const wxString&)> writeTree = [&]( git_tree* t,
const wxString& base )
1049 size_t ecnt = git_tree_entrycount( t );
1050 for(
size_t i = 0; i < ecnt; ++i )
1052 const git_tree_entry* e = git_tree_entry_byindex( t, i );
1053 wxString
name = wxString::FromUTF8( git_tree_entry_name( e ) );
1055 if( git_tree_entry_type( e ) == GIT_OBJECT_TREE )
1057 wxFileName dir( base,
name );
1058 wxMkdir( dir.GetFullPath() );
1059 git_tree* sub =
nullptr;
1061 if( git_tree_lookup( &sub, repo, git_tree_entry_id( e ) ) == 0 )
1063 writeTree( sub, dir.GetFullPath() );
1064 git_tree_free( sub );
1067 else if( git_tree_entry_type( e ) == GIT_OBJECT_BLOB )
1069 git_blob* blob =
nullptr;
1071 if( git_blob_lookup( &blob, repo, git_tree_entry_id( e ) ) == 0 )
1073 wxFileName file( base,
name );
1074 wxFFile f( file.GetFullPath(), wxT(
"wb") );
1078 f.Write( (
const char*) git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1081 git_blob_free( blob );
1087 writeTree( tree, trimPath );
1089 git_index* newIndex =
nullptr;
1090 git_repository_index( &newIndex, newRepo );
1091 git_index_add_all( newIndex,
nullptr, 0,
nullptr,
nullptr );
1092 git_index_write( newIndex );
1094 git_index_write_tree( &newTreeOid, newIndex );
1095 git_tree* newTree =
nullptr;
1096 git_tree_lookup( &newTree, newRepo, &newTreeOid );
1099 const git_signature* origAuthor = git_commit_author( orig );
1100 const git_signature* origCommitter = git_commit_committer( orig );
1101 git_signature* sigAuthor =
nullptr;
1102 git_signature* sigCommitter =
nullptr;
1104 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1105 origAuthor->when.time, origAuthor->when.offset );
1106 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1107 origCommitter->when.time, origCommitter->when.offset );
1109 const git_commit* parents[1];
1110 int parentCount = 0;
1114 parents[0] = parent;
1118 git_oid newCommitOid;
1119 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1120 newTree, parentCount, parentCount ? parents :
nullptr );
1122 git_commit_free( parent );
1124 git_commit_lookup( &parent, newRepo, &newCommitOid );
1126 commitMap.emplace_back( co, newCommitOid );
1128 git_signature_free( sigAuthor );
1129 git_signature_free( sigCommitter );
1130 git_tree_free( newTree );
1131 git_index_free( newIndex );
1132 git_tree_free( tree );
1133 git_commit_free( orig );
1137 git_commit_free( parent );
1140 for(
const auto& tt : tagTargets )
1143 const git_oid* newOid =
nullptr;
1145 for(
const auto& m : commitMap )
1147 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1157 git_object* obj =
nullptr;
1159 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1161 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1162 git_object_free( obj );
1169 git_repository_free( newRepo );
1172 wxString backupOld = hist + wxS(
"_old");
1173 wxRenameFile( hist, backupOld );
1174 wxRenameFile( trimPath, hist );
1175 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1182 git_repository* repo =
nullptr;
1184 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1185 return wxEmptyString;
1188 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1190 git_repository_free( repo );
1191 return wxEmptyString;
1194 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1195 git_repository_free( repo );
1207bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1209 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
1211 wxDir dir( dirPath );
1212 if( !dir.IsOpened() )
1216 bool cont = dir.GetFirst( &filename );
1220 wxFileName fullPath( dirPath, filename );
1223 if( filename == wxS(
".history") || filename == wxS(
".git") )
1225 cont = dir.GetNext( &filename );
1229 if( fullPath.DirExists() )
1231 findLocks( fullPath.GetFullPath() );
1233 else if( fullPath.FileExists()
1240 baseName = baseName.BeforeLast(
'.' );
1241 wxFileName originalFile( dirPath, baseName );
1244 LOCKFILE testLock( originalFile.GetFullPath() );
1245 if( testLock.Valid() && !testLock.IsLockedByMe() )
1247 aLockedFiles.push_back( fullPath.GetFullPath() );
1251 cont = dir.GetNext( &filename );
1255 findLocks( aProjectPath );
1256 return aLockedFiles.empty();
1263bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
1265 bool extractSuccess =
true;
1267 std::function<void( git_tree*,
const wxString& )> extractTree =
1268 [&]( git_tree* t,
const wxString& prefix )
1270 if( !extractSuccess )
1273 size_t cnt = git_tree_entrycount( t );
1274 for(
size_t i = 0; i < cnt; ++i )
1276 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1277 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1278 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
1280 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1282 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
1284 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1287 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
1288 dirPath.GetPath() );
1289 extractSuccess =
false;
1293 git_tree* sub =
nullptr;
1294 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
1296 extractTree( sub, fullPath );
1297 git_tree_free( sub );
1300 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1302 git_blob* blob =
nullptr;
1303 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
1305 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
1307 wxFileName dstDir( dst );
1308 dstDir.SetFullName( wxEmptyString );
1309 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1311 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
1314 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1320 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
1321 dst.GetFullPath() );
1322 extractSuccess =
false;
1323 git_blob_free( blob );
1327 git_blob_free( blob );
1333 extractTree( aTree, wxEmptyString );
1334 return extractSuccess;
1341void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
1342 std::set<wxString>& aFiles )
1344 wxDir dir( aSearchPath );
1345 if( !dir.IsOpened() )
1349 bool cont = dir.GetFirst( &filename );
1353 wxFileName fullPath( aSearchPath, filename );
1354 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
1356 if( fullPath.IsDir() && fullPath.DirExists() )
1358 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
1360 else if( fullPath.FileExists() )
1362 aFiles.insert( relativePath );
1365 cont = dir.GetNext( &filename );
1373bool shouldExcludeFromBackup(
const wxString& aFilename )
1376 return aFilename == wxS(
"fp-info-cache" );
1383void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
1384 std::vector<wxString>& aFilesToDelete )
1386 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
1387 [&](
const wxString& dirPath,
const wxString& relativeBase )
1389 wxDir dir( dirPath );
1390 if( !dir.IsOpened() )
1394 bool cont = dir.GetFirst( &filename );
1399 if( filename == wxS(
".history") || filename == wxS(
".git") ||
1400 filename == wxS(
"_restore_backup") || filename == wxS(
"_restore_temp") )
1402 cont = dir.GetNext( &filename );
1406 wxFileName fullPath( dirPath, filename );
1407 wxString relativePath = relativeBase.IsEmpty() ? filename :
1408 relativeBase + wxS(
"/") + filename;
1410 if( fullPath.IsDir() && fullPath.DirExists() )
1412 scanDirectory( fullPath.GetFullPath(), relativePath );
1414 else if( fullPath.FileExists() )
1417 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
1420 if( !shouldExcludeFromBackup( filename ) )
1421 aFilesToDelete.push_back( relativePath );
1425 cont = dir.GetNext( &filename );
1429 scanDirectory( aProjectPath, wxEmptyString );
1437bool confirmFileDeletion( wxWindow* aParent,
const std::vector<wxString>& aFilesToDelete,
1438 bool& aKeepAllFiles )
1440 if( aFilesToDelete.empty() || !aParent )
1442 aKeepAllFiles =
false;
1446 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
1449 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
1450 for(
size_t i = 0; i < displayCount; ++i )
1452 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
1455 if( aFilesToDelete.size() > displayCount )
1457 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
1458 aFilesToDelete.size() - displayCount );
1461 wxMessageDialog dlg( aParent, message,
_(
"Delete Files during Restore" ),
1462 wxYES_NO | wxCANCEL | wxICON_QUESTION );
1463 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
1464 dlg.SetExtendedMessage(
1465 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
1466 "files in the project directory. Choosing 'Proceed' will delete files that are not "
1467 "present in the restored commit." ) );
1469 int choice = dlg.ShowModal();
1471 if( choice == wxID_CANCEL )
1473 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
1476 else if( choice == wxID_NO )
1478 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
1479 aKeepAllFiles =
true;
1483 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
1484 aKeepAllFiles =
false;
1494bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
1495 const wxString& aTempRestorePath,
bool aKeepAllFiles,
1496 std::set<wxString>& aBackedUpFiles )
1498 wxDir currentDir( aProjectPath );
1499 if( !currentDir.IsOpened() )
1503 bool cont = currentDir.GetFirst( &filename );
1507 if( filename != wxS(
".history" ) && filename != wxS(
".git" ) &&
1508 filename != wxS(
"_restore_backup" ) && filename != wxS(
"_restore_temp" ) )
1511 bool shouldBackup = !aKeepAllFiles;
1516 wxFileName testPath( aTempRestorePath, filename );
1517 shouldBackup = testPath.Exists();
1522 wxFileName source( aProjectPath, filename );
1523 wxFileName dest( aBackupPath, filename );
1526 if( !wxDirExists( aBackupPath ) )
1529 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
1531 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
1535 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
1536 source.GetFullPath(), dest.GetFullPath() );
1538 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1541 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
1542 source.GetFullPath() );
1546 aBackedUpFiles.insert( filename );
1549 cont = currentDir.GetNext( &filename );
1559bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
1560 std::set<wxString>& aRestoredFiles )
1562 wxDir tempDir( aTempRestorePath );
1563 if( !tempDir.IsOpened() )
1567 bool cont = tempDir.GetFirst( &filename );
1571 wxFileName source( aTempRestorePath, filename );
1572 wxFileName dest( aProjectPath, filename );
1575 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
1576 source.GetFullPath(), dest.GetFullPath() );
1578 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1581 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
1582 source.GetFullPath() );
1586 aRestoredFiles.insert( filename );
1587 cont = tempDir.GetNext( &filename );
1597void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
1598 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
1599 const std::set<wxString>& aRestoredFiles )
1601 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
1605 for(
const wxString& filename : aRestoredFiles )
1607 wxFileName toRemove( aProjectPath, filename );
1608 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
1609 toRemove.GetFullPath() );
1611 if( toRemove.DirExists() )
1613 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1615 else if( toRemove.FileExists() )
1617 wxRemoveFile( toRemove.GetFullPath() );
1622 if( wxDirExists( aBackupPath ) )
1624 for(
const wxString& filename : aBackedUpFiles )
1626 wxFileName source( aBackupPath, filename );
1627 wxFileName dest( aProjectPath, filename );
1629 if( source.Exists() )
1631 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1632 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
1633 dest.GetFullPath() );
1639 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
1640 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
1647bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
1648 const wxString& aHash )
1650 git_time_t t = git_commit_time( aCommit );
1651 wxDateTime dt( (time_t) t );
1652 git_signature* sig =
nullptr;
1654 git_commit* parent =
nullptr;
1657 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
1658 git_commit_lookup( &parent, aRepo, &parent_id );
1661 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1664 const git_commit* constParent = parent;
1665 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
1666 msg.mb_str().data(), aTree, parent ? 1 : 0,
1667 parent ? &constParent :
nullptr );
1670 git_commit_free( parent );
1671 git_signature_free( sig );
1683 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
1686 std::vector<wxString> lockedFiles;
1687 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
1690 for(
const auto& f : lockedFiles )
1691 lockList += wxS(
"\n - ") + f;
1694 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
1700 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
1702 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
1713 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
1724 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
1726 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
1730 git_commit* commit =
nullptr;
1731 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
1733 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
1737 git_tree* tree =
nullptr;
1738 git_commit_tree( &tree, commit );
1741 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
1743 std::vector<wxString> backupFiles;
1746 if( !backupFiles.empty() )
1750 wxS(
"Pre-restore backup" ) ) )
1753 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
1754 git_tree_free( tree );
1755 git_commit_free( commit );
1761 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
1763 if( wxDirExists( tempRestorePath ) )
1764 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1766 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
1769 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
1771 git_tree_free( tree );
1772 git_commit_free( commit );
1776 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
1779 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
1781 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
1782 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1783 git_tree_free( tree );
1784 git_commit_free( commit );
1789 std::set<wxString> restoredFiles;
1790 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
1792 std::vector<wxString> filesToDelete;
1793 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
1795 bool keepAllFiles =
true;
1796 if( !confirmFileDeletion( aParent, filesToDelete, keepAllFiles ) )
1799 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1800 git_tree_free( tree );
1801 git_commit_free( commit );
1806 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
1808 wxString backupPath = aProjectPath + wxS(
"_restore_backup");
1811 if( wxDirExists( backupPath ) )
1813 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Removing old backup %s" ),
1815 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1819 std::set<wxString> backedUpFiles;
1820 std::set<wxString> restoredFilesSet;
1823 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
1826 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1828 git_tree_free( tree );
1829 git_commit_free( commit );
1834 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
1836 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1838 git_tree_free( tree );
1839 git_commit_free( commit );
1844 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Restore successful, cleaning up" ) );
1845 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1846 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1849 recordRestoreInHistory( repo, commit, tree, aHash );
1851 git_tree_free( tree );
1852 git_commit_free( commit );
1854 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
1864 git_repository* repo =
nullptr;
1866 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1869 git_revwalk* walk =
nullptr;
1870 git_revwalk_new( &walk, repo );
1871 git_revwalk_push_head( walk );
1873 std::vector<wxString> choices;
1874 std::vector<wxString> hashes;
1877 while( git_revwalk_next( &oid, walk ) == 0 )
1879 git_commit* commit =
nullptr;
1880 git_commit_lookup( &commit, repo, &oid );
1882 git_time_t t = git_commit_time( commit );
1883 wxDateTime dt( (time_t) t );
1886 line.Printf( wxS(
"%s %s" ), dt.FormatISOCombined().c_str(),
1887 wxString::FromUTF8( git_commit_summary( commit ) ) );
1888 choices.push_back( line );
1889 hashes.push_back( wxString::FromUTF8( git_oid_tostr_s( &oid ) ) );
1890 git_commit_free( commit );
1893 git_revwalk_free( walk );
1894 git_repository_free( repo );
1896 if( choices.empty() )
1899 int index = wxGetSingleChoiceIndex(
_(
"Select snapshot" ),
_(
"Restore" ),
1900 (
int) choices.size(), &choices[0], aParent );
1902 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.
static const std::string LockFileExtension
static const std::string LockFilePrefix
const wxChar *const traceAutoSave
Flag to enable auto save feature debug tracing.
static wxString historyPath(const wxString &aProjectPath)
static wxString historyPath(const wxString &aProjectPath)
static size_t dirSizeRecursive(const wxString &path)
static bool commitSnapshotWithLock(git_repository *repo, git_index *index, const wxString &aHistoryPath, const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
static void collectProjectFiles(const wxString &aProjectPath, std::vector< wxString > &aFiles)
PGM_BASE & Pgm()
The global program "get" accessor.
wxString result
Test unit parsing edge cases and error handling.
wxLogTrace helper definitions.
Definition of file extensions used in Kicad.