34#include <wx/filename.h>
38#include <wx/datetime.h>
41#include <wx/choicdlg.h>
54 wxFileName p( aProjectPath, wxEmptyString );
55 p.AppendDir( wxS(
".history" ) );
69 wxFileName fn( aFile );
71 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
79 const std::function<
void(
const wxString&, std::vector<wxString>& )>& aSaver )
83 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping"), aSaverObject );
88 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
94 auto it =
m_savers.find( aSaverObject );
99 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
107 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers") );
113 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
115 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
119 wxLogTrace(
traceAutoSave, wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu"),
120 aProjectPath, aTitle,
m_savers.size() );
124 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
128 std::vector<wxString> files;
130 for(
const auto& [saverObject, saver] :
m_savers )
132 size_t before = files.size();
133 saver( aProjectPath, files );
134 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p added %zu files (total=%zu)"),
135 saverObject, files.size() - before, files.size() );
139 wxString projectDir = aProjectPath;
140 if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
141 projectDir += wxFileName::GetPathSeparator();
143 auto it = std::remove_if( files.begin(), files.end(),
144 [&projectDir](
const wxString& file )
146 if( !file.StartsWith( projectDir ) )
148 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out file outside project: %s"), file );
153 files.erase( it, files.end() );
157 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no files; skipping") );
164 if( !lock.IsLocked() )
166 wxLogTrace(
traceAutoSave, wxS(
"[history] failed to acquire lock: %s"), lock.GetLockError() );
170 git_repository* repo = lock.GetRepository();
171 git_index*
index = lock.GetIndex();
175 for(
const wxString& file : files )
177 wxFileName src( file );
179 if( !src.FileExists() )
181 wxLogTrace(
traceAutoSave, wxS(
"[history] skip missing '%s'"), file );
186 if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
188 std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
189 git_index_add_bypath(
index, relHist.c_str() );
190 wxLogTrace(
traceAutoSave, wxS(
"[history] staged pre-mirrored '%s'"), file );
195 wxString proj = wxFileName( aProjectPath ).GetFullPath();
197 if( src.GetFullPath().StartsWith( proj + wxFILE_SEP_PATH ) )
198 relStr = src.GetFullPath().Mid( proj.length() + 1 );
200 relStr = src.GetFullName();
202 wxFileName dst( hist + wxFILE_SEP_PATH + relStr );
203 wxFileName dstDir( dst );
204 dstDir.SetFullName( wxEmptyString );
205 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
206 wxCopyFile( src.GetFullPath(), dst.GetFullPath(),
true );
207 std::string rel = dst.GetFullPath().ToStdString().substr( hist.length() + 1 );
208 git_index_add_bypath(
index, rel.c_str() );
209 wxLogTrace(
traceAutoSave, wxS(
"[history] staged '%s' as '%s'"), file, wxString::FromUTF8( rel ) );
214 git_commit* head_commit =
nullptr;
215 git_tree* head_tree =
nullptr;
217 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
218 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
219 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
221 git_tree* rawIndexTree =
nullptr;
222 git_oid index_tree_oid;
224 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
226 if( head_tree ) git_tree_free( head_tree );
227 if( head_commit ) git_commit_free( head_commit );
228 wxLogTrace(
traceAutoSave, wxS(
"[history] failed to write index tree" ) );
232 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
233 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
235 bool hasChanges =
true;
239 git_diff* diff =
nullptr;
241 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
243 hasChanges = git_diff_num_deltas( diff ) > 0;
244 wxLogTrace(
traceAutoSave, wxS(
"[history] diff deltas=%u"), (
unsigned) git_diff_num_deltas( diff ) );
245 git_diff_free( diff );
249 if( head_tree ) git_tree_free( head_tree );
250 if( head_commit ) git_commit_free( head_commit );
254 wxLogTrace(
traceAutoSave, wxS(
"[history] no changes detected; no commit") );
258 git_signature* rawSig =
nullptr;
260 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
262 git_commit* parent =
nullptr;
266 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
268 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
272 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
274 const git_commit* constParent = parent;
276 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
277 msg.mb_str().data(), indexTree.get(), parents,
278 parents ? &constParent :
nullptr );
281 wxLogTrace(
traceAutoSave, wxS(
"[history] commit created %s (%s files=%zu)"),
282 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, files.size() );
284 wxLogTrace(
traceAutoSave, wxS(
"[history] commit failed rc=%d"), rc );
286 if( parent ) git_commit_free( parent );
288 git_index_write(
index );
303 if( aProjectPath.IsEmpty() )
306 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
311 if( !wxDirExists( hist ) )
313 if( wxIsWritable( aProjectPath ) )
315 if( !wxMkdir( hist ) )
322 git_repository* rawRepo =
nullptr;
323 bool isNewRepo =
false;
325 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
327 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
332 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
333 if( !ignoreFile.FileExists() )
335 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
338 f.Write( wxS(
"fp-info-cache\n*-backups\nREADME.txt\n" ) );
343 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
345 if( !readmeFile.FileExists() )
347 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
351 f.Write( wxS(
"KiCad Local History Directory\n"
352 "=============================\n\n"
353 "This directory contains automatic snapshots of your project files.\n"
354 "KiCad periodically saves copies of your work here, allowing you to\n"
355 "recover from accidental changes or data loss.\n\n"
356 "You can browse and restore previous versions through KiCad's\n"
357 "File > Local History menu.\n\n"
358 "To disable this feature:\n"
359 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
360 "This directory can be safely deleted if you no longer need the\n"
361 "history, but doing so will permanently remove all saved snapshots.\n" ) );
367 git_repository_free( rawRepo );
373 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: New repository created, collecting existing files" ) );
377 std::function<void(
const wxString& )> collect = [&](
const wxString&
path )
385 bool cont = d.GetFirst( &
name );
390 if(
name.StartsWith( wxS(
"." ) ) ||
name.EndsWith( wxS(
"-backups" ) ) )
392 cont = d.GetNext( &
name );
397 wxString fullPath = fn.GetFullPath();
399 if( wxFileName::DirExists( fullPath ) )
403 else if( fn.FileExists() )
406 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
407 files.Add( fn.GetFullPath() );
410 cont = d.GetNext( &
name );
414 collect( aProjectPath );
416 if( files.GetCount() > 0 )
418 std::vector<wxString> vec;
419 vec.reserve( files.GetCount() );
421 for(
unsigned i = 0; i < files.GetCount(); ++i )
422 vec.push_back( files[i] );
424 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: Creating initial snapshot with %zu files" ), vec.size() );
429 TagSave( aProjectPath, wxS(
"project" ) );
433 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: No files found to add to initial snapshot" ) );
443 const wxString& aHistoryPath,
const wxString& aProjectPath,
444 const std::vector<wxString>& aFiles,
const wxString& aTitle )
446 std::vector<std::string> filesArrStr;
448 for(
const wxString& file : aFiles )
450 wxFileName src( file );
453 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
454 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
456 relPath = src.GetFullName();
458 relPath.Replace(
"\\",
"/" );
459 std::string relPathStr = relPath.ToStdString();
461 unsigned int status = 0;
462 int rc = git_status_file( &status, repo, relPathStr.data() );
464 if( rc == 0 && status != 0 )
466 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
467 filesArrStr.emplace_back( relPathStr );
471 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
472 filesArrStr.emplace_back( relPathStr );
476 std::vector<char*> cStrings( filesArrStr.size() );
478 for(
size_t i = 0; i < filesArrStr.size(); i++ )
479 cStrings[i] = filesArrStr[i].data();
481 git_strarray filesArrGit;
482 filesArrGit.count = filesArrStr.size();
483 filesArrGit.strings = cStrings.data();
485 if( filesArrStr.size() == 0 )
491 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
493 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
499 if( git_index_write_tree( &tree_id,
index ) != 0 )
502 git_tree* rawTree =
nullptr;
503 git_tree_lookup( &rawTree, repo, &tree_id );
504 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
506 git_signature* rawSig =
nullptr;
508 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
509 &git_signature_free );
511 git_commit* rawParent =
nullptr;
515 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
517 git_commit_lookup( &rawParent, repo, &parent_id );
521 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
524 git_tree* rawParentTree =
nullptr;
527 git_commit_tree( &rawParentTree, parent.get() );
529 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
531 git_diff* rawDiff =
nullptr;
532 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
533 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
535 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
537 if( numChangedFiles == 0 )
539 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
545 if( !aTitle.IsEmpty() )
546 msg << aTitle << wxS(
": " );
548 msg << numChangedFiles << wxS(
" files changed" );
550 for(
size_t i = 0; i < numChangedFiles; ++i )
552 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
553 git_patch* rawPatch =
nullptr;
554 git_patch_from_diff( &rawPatch, diff.get(), i );
555 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
557 size_t context = 0, adds = 0, dels = 0;
558 git_patch_line_stats( &context, &adds, &dels, patch.get() );
559 size_t updated = std::min( adds, dels );
562 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
563 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
567 git_commit* parentPtr = parent.get();
568 const git_commit* constParentPtr = parentPtr;
569 git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
570 msg.mb_str().data(), tree.get(), parents,
571 parentPtr ? &constParentPtr :
nullptr );
572 git_index_write(
index );
579 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled )
582 wxString proj = wxFileName( aFiles[0] ).GetPath();
590 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitSnapshot failed to acquire lock: %s"),
605 wxDir dir( aProjectPath );
607 if( !dir.IsOpened() )
611 std::function<void(
const wxString&)> collect = [&](
const wxString&
path )
619 bool cont = d.GetFirst( &
name );
623 if(
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) )
625 cont = d.GetNext( &
name );
630 wxString fullPath = fn.GetFullPath();
632 if( wxFileName::DirExists( fullPath ) )
636 else if( fn.FileExists() )
639 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
640 aFiles.push_back( fn.GetFullPath() );
643 cont = d.GetNext( &
name );
647 collect( aProjectPath );
653 std::vector<wxString> files;
673 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
683 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
688 git_reference* ref =
nullptr;
691 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
692 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
695 git_object* head_obj =
nullptr;
696 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
697 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
698 git_object_free( head_obj );
701 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
702 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
704 git_reference_delete( ref );
705 git_reference_free( ref );
708 git_oid last_tag_oid;
709 git_object* head_obj2 =
nullptr;
710 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
711 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
712 git_object_free( head_obj2 );
720 git_repository* repo =
nullptr;
722 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
726 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
728 git_repository_free( repo );
732 git_commit* head_commit =
nullptr;
733 git_commit_lookup( &head_commit, repo, &head_oid );
734 git_time_t head_time = git_commit_time( head_commit );
737 git_tag_list_match( &tags,
"Last_Save_*", repo );
738 git_time_t save_time = 0;
740 for(
size_t i = 0; i < tags.count; ++i )
742 git_reference* ref =
nullptr;
743 if( git_reference_lookup( &ref, repo,
744 ( wxS(
"refs/tags/" ) +
745 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
747 const git_oid* oid = git_reference_target( ref );
748 git_commit* c =
nullptr;
749 if( git_commit_lookup( &c, repo, oid ) == 0 )
751 git_time_t t = git_commit_time( c );
754 git_commit_free( c );
756 git_reference_free( ref );
760 git_strarray_free( &tags );
761 git_commit_free( head_commit );
762 git_repository_free( repo );
769 return head_time > save_time;
773 const wxString& aMessage )
779 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
788 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
789 git_reference* lastRef =
nullptr;
790 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
792 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
794 const git_oid* lastOid = git_reference_target( lastRef );
795 git_commit* lastCommit =
nullptr;
796 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
798 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
800 git_tree* lastTree =
nullptr;
801 git_commit_tree( &lastTree, lastCommit );
802 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
806 git_commit* headCommit =
nullptr;
808 const git_commit* parentArray[1];
809 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
810 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
812 parentArray[0] = headCommit;
816 git_signature* sigRaw =
nullptr;
818 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
820 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
821 git_oid newCommitOid;
822 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
823 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
824 if( headCommit ) git_commit_free( headCommit );
829 git_reference* existing =
nullptr;
830 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
832 git_reference_delete( existing );
833 git_reference_free( existing );
835 git_object* newCommitObj =
nullptr;
836 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
838 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
839 git_object_free( newCommitObj );
848 if( !dir.IsOpened() )
851 bool cont = dir.GetFirst( &
name );
855 wxString fullPath = fn.GetFullPath();
857 if( wxFileName::DirExists( fullPath ) )
859 else if( fn.FileExists() )
860 total += (size_t) fn.GetSize().GetValue();
861 cont = dir.GetNext( &
name );
873 if( !wxDirExists( hist ) )
878 if( current <= aMaxBytes )
885 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
895 git_revwalk* walk =
nullptr;
896 git_revwalk_new( &walk, repo );
897 git_revwalk_sorting( walk, GIT_SORT_TIME );
898 git_revwalk_push_head( walk );
899 std::vector<git_oid> commits;
902 while( git_revwalk_next( &oid, walk ) == 0 )
903 commits.push_back( oid );
905 git_revwalk_free( walk );
907 if( commits.empty() )
911 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
912 [](
const git_oid& a,
const git_oid& b )
914 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
917 size_t keptBytes = 0;
918 std::vector<git_oid> keep;
920 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
923 size_t cnt = git_tree_entrycount( tree );
924 for(
size_t i = 0; i < cnt; ++i )
926 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
928 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
930 const git_oid* bid = git_tree_entry_id( entry );
932 if( seenBlobs.find( *bid ) == seenBlobs.end() )
934 git_blob* blob =
nullptr;
936 if( git_blob_lookup( &blob, repo, bid ) == 0 )
938 added += git_blob_rawsize( blob );
939 git_blob_free( blob );
942 seenBlobs.insert( *bid );
945 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
947 git_tree* sub =
nullptr;
949 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
951 added += accountTree( sub );
952 git_tree_free( sub );
959 for(
const git_oid& cOid : commits )
961 git_commit* c =
nullptr;
963 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
966 git_tree* tree =
nullptr;
967 git_commit_tree( &tree, c );
968 size_t add = accountTree( tree );
969 git_tree_free( tree );
970 git_commit_free( c );
972 if( keep.empty() || keptBytes + add <= aMaxBytes )
974 keep.push_back( cOid );
982 keep.push_back( commits.front() );
986 std::vector<std::pair<wxString, git_oid>> tagTargets;
987 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
988 [](
const git_oid& a,
const git_oid& b )
990 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
992 git_strarray tagList;
994 if( git_tag_list( &tagList, repo ) == 0 )
996 for(
size_t i = 0; i < tagList.count; ++i )
998 wxString
name = wxString::FromUTF8( tagList.strings[i] );
999 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1001 git_reference* tref =
nullptr;
1003 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1005 const git_oid* toid = git_reference_target( tref );
1009 tagTargets.emplace_back(
name, *toid );
1010 taggedCommits.insert( *toid );
1014 for(
const auto& k : keep )
1016 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1026 keep.push_back( *toid );
1027 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1032 git_reference_free( tref );
1036 git_strarray_free( &tagList );
1040 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1041 wxString trimPath = trimFn.GetPath();
1043 if( wxDirExists( trimPath ) )
1044 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1046 wxMkdir( trimPath );
1047 git_repository* newRepo =
nullptr;
1049 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1053 std::reverse( keep.begin(), keep.end() );
1054 git_commit* parent =
nullptr;
1055 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1056 std::vector<MAP_ENTRY> commitMap;
1058 for(
const git_oid& co : keep )
1060 git_commit* orig =
nullptr;
1062 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1065 git_tree* tree =
nullptr;
1066 git_commit_tree( &tree, orig );
1070 wxArrayString toDelete;
1071 wxDir d( trimPath );
1073 bool cont = d.GetFirst( &nm );
1076 if( nm != wxS(
".git") )
1078 cont = d.GetNext( &nm );
1081 for(
auto& del : toDelete )
1083 wxFileName f( trimPath, del );
1085 wxFileName::Rmdir( f.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1086 else if( f.FileExists() )
1087 wxRemoveFile( f.GetFullPath() );
1091 std::function<void(git_tree*,
const wxString&)> writeTree = [&]( git_tree* t,
const wxString& base )
1093 size_t ecnt = git_tree_entrycount( t );
1094 for(
size_t i = 0; i < ecnt; ++i )
1096 const git_tree_entry* e = git_tree_entry_byindex( t, i );
1097 wxString
name = wxString::FromUTF8( git_tree_entry_name( e ) );
1099 if( git_tree_entry_type( e ) == GIT_OBJECT_TREE )
1101 wxFileName dir( base,
name );
1102 wxMkdir( dir.GetFullPath() );
1103 git_tree* sub =
nullptr;
1105 if( git_tree_lookup( &sub, repo, git_tree_entry_id( e ) ) == 0 )
1107 writeTree( sub, dir.GetFullPath() );
1108 git_tree_free( sub );
1111 else if( git_tree_entry_type( e ) == GIT_OBJECT_BLOB )
1113 git_blob* blob =
nullptr;
1115 if( git_blob_lookup( &blob, repo, git_tree_entry_id( e ) ) == 0 )
1117 wxFileName file( base,
name );
1118 wxFFile f( file.GetFullPath(), wxT(
"wb") );
1122 f.Write( (
const char*) git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1125 git_blob_free( blob );
1131 writeTree( tree, trimPath );
1133 git_index* newIndex =
nullptr;
1134 git_repository_index( &newIndex, newRepo );
1135 git_index_add_all( newIndex,
nullptr, 0,
nullptr,
nullptr );
1136 git_index_write( newIndex );
1138 git_index_write_tree( &newTreeOid, newIndex );
1139 git_tree* newTree =
nullptr;
1140 git_tree_lookup( &newTree, newRepo, &newTreeOid );
1143 const git_signature* origAuthor = git_commit_author( orig );
1144 const git_signature* origCommitter = git_commit_committer( orig );
1145 git_signature* sigAuthor =
nullptr;
1146 git_signature* sigCommitter =
nullptr;
1148 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1149 origAuthor->when.time, origAuthor->when.offset );
1150 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1151 origCommitter->when.time, origCommitter->when.offset );
1153 const git_commit* parents[1];
1154 int parentCount = 0;
1158 parents[0] = parent;
1162 git_oid newCommitOid;
1163 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1164 newTree, parentCount, parentCount ? parents :
nullptr );
1166 git_commit_free( parent );
1168 git_commit_lookup( &parent, newRepo, &newCommitOid );
1170 commitMap.emplace_back( co, newCommitOid );
1172 git_signature_free( sigAuthor );
1173 git_signature_free( sigCommitter );
1174 git_tree_free( newTree );
1175 git_index_free( newIndex );
1176 git_tree_free( tree );
1177 git_commit_free( orig );
1181 git_commit_free( parent );
1184 for(
const auto& tt : tagTargets )
1187 const git_oid* newOid =
nullptr;
1189 for(
const auto& m : commitMap )
1191 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1201 git_object* obj =
nullptr;
1203 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1205 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1206 git_object_free( obj );
1213 git_repository_free( newRepo );
1216 wxString backupOld = hist + wxS(
"_old");
1217 wxRenameFile( hist, backupOld );
1218 wxRenameFile( trimPath, hist );
1219 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1226 git_repository* repo =
nullptr;
1228 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1229 return wxEmptyString;
1232 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1234 git_repository_free( repo );
1235 return wxEmptyString;
1238 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1239 git_repository_free( repo );
1251bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1253 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
1255 wxDir dir( dirPath );
1256 if( !dir.IsOpened() )
1260 bool cont = dir.GetFirst( &filename );
1264 wxFileName fullPath( dirPath, filename );
1267 if( filename == wxS(
".history") || filename == wxS(
".git") )
1269 cont = dir.GetNext( &filename );
1273 if( fullPath.DirExists() )
1275 findLocks( fullPath.GetFullPath() );
1277 else if( fullPath.FileExists()
1284 baseName = baseName.BeforeLast(
'.' );
1285 wxFileName originalFile( dirPath, baseName );
1288 LOCKFILE testLock( originalFile.GetFullPath() );
1289 if( testLock.Valid() && !testLock.IsLockedByMe() )
1291 aLockedFiles.push_back( fullPath.GetFullPath() );
1295 cont = dir.GetNext( &filename );
1299 findLocks( aProjectPath );
1300 return aLockedFiles.empty();
1307bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
1309 bool extractSuccess =
true;
1311 std::function<void( git_tree*,
const wxString& )> extractTree =
1312 [&]( git_tree* t,
const wxString& prefix )
1314 if( !extractSuccess )
1317 size_t cnt = git_tree_entrycount( t );
1318 for(
size_t i = 0; i < cnt; ++i )
1320 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1321 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1322 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
1324 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1326 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
1328 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1331 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
1332 dirPath.GetPath() );
1333 extractSuccess =
false;
1337 git_tree* sub =
nullptr;
1338 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
1340 extractTree( sub, fullPath );
1341 git_tree_free( sub );
1344 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1346 git_blob* blob =
nullptr;
1347 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
1349 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
1351 wxFileName dstDir( dst );
1352 dstDir.SetFullName( wxEmptyString );
1353 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1355 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
1358 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1364 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
1365 dst.GetFullPath() );
1366 extractSuccess =
false;
1367 git_blob_free( blob );
1371 git_blob_free( blob );
1377 extractTree( aTree, wxEmptyString );
1378 return extractSuccess;
1385void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
1386 std::set<wxString>& aFiles )
1388 wxDir dir( aSearchPath );
1389 if( !dir.IsOpened() )
1393 bool cont = dir.GetFirst( &filename );
1397 wxFileName fullPath( aSearchPath, filename );
1398 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
1400 if( fullPath.IsDir() && fullPath.DirExists() )
1402 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
1404 else if( fullPath.FileExists() )
1406 aFiles.insert( relativePath );
1409 cont = dir.GetNext( &filename );
1417bool shouldExcludeFromBackup(
const wxString& aFilename )
1420 return aFilename == wxS(
"fp-info-cache" );
1427void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
1428 std::vector<wxString>& aFilesToDelete )
1430 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
1431 [&](
const wxString& dirPath,
const wxString& relativeBase )
1433 wxDir dir( dirPath );
1434 if( !dir.IsOpened() )
1438 bool cont = dir.GetFirst( &filename );
1443 if( filename == wxS(
".history") || filename == wxS(
".git") ||
1444 filename == wxS(
"_restore_backup") || filename == wxS(
"_restore_temp") )
1446 cont = dir.GetNext( &filename );
1450 wxFileName fullPath( dirPath, filename );
1451 wxString relativePath = relativeBase.IsEmpty() ? filename :
1452 relativeBase + wxS(
"/") + filename;
1454 if( fullPath.IsDir() && fullPath.DirExists() )
1456 scanDirectory( fullPath.GetFullPath(), relativePath );
1458 else if( fullPath.FileExists() )
1461 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
1464 if( !shouldExcludeFromBackup( filename ) )
1465 aFilesToDelete.push_back( relativePath );
1469 cont = dir.GetNext( &filename );
1473 scanDirectory( aProjectPath, wxEmptyString );
1481bool confirmFileDeletion( wxWindow* aParent,
const std::vector<wxString>& aFilesToDelete,
1482 bool& aKeepAllFiles )
1484 if( aFilesToDelete.empty() || !aParent )
1486 aKeepAllFiles =
false;
1490 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
1493 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
1494 for(
size_t i = 0; i < displayCount; ++i )
1496 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
1499 if( aFilesToDelete.size() > displayCount )
1501 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
1502 aFilesToDelete.size() - displayCount );
1506 wxYES_NO | wxCANCEL | wxICON_QUESTION );
1507 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
1508 dlg.SetExtendedMessage(
1509 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
1510 "files in the project directory. Choosing 'Proceed' will delete files that are not "
1511 "present in the restored commit." ) );
1513 int choice = dlg.ShowModal();
1515 if( choice == wxID_CANCEL )
1517 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
1520 else if( choice == wxID_NO )
1522 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
1523 aKeepAllFiles =
true;
1527 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
1528 aKeepAllFiles =
false;
1538bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
1539 const wxString& aTempRestorePath,
bool aKeepAllFiles,
1540 std::set<wxString>& aBackedUpFiles )
1542 wxDir currentDir( aProjectPath );
1543 if( !currentDir.IsOpened() )
1547 bool cont = currentDir.GetFirst( &filename );
1551 if( filename != wxS(
".history" ) && filename != wxS(
".git" ) &&
1552 filename != wxS(
"_restore_backup" ) && filename != wxS(
"_restore_temp" ) )
1555 bool shouldBackup = !aKeepAllFiles;
1560 wxFileName testPath( aTempRestorePath, filename );
1561 shouldBackup = testPath.Exists();
1566 wxFileName source( aProjectPath, filename );
1567 wxFileName dest( aBackupPath, filename );
1570 if( !wxDirExists( aBackupPath ) )
1573 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
1575 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
1579 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
1580 source.GetFullPath(), dest.GetFullPath() );
1582 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1585 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
1586 source.GetFullPath() );
1590 aBackedUpFiles.insert( filename );
1593 cont = currentDir.GetNext( &filename );
1603bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
1604 std::set<wxString>& aRestoredFiles )
1606 wxDir tempDir( aTempRestorePath );
1607 if( !tempDir.IsOpened() )
1611 bool cont = tempDir.GetFirst( &filename );
1615 wxFileName source( aTempRestorePath, filename );
1616 wxFileName dest( aProjectPath, filename );
1619 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
1620 source.GetFullPath(), dest.GetFullPath() );
1622 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1625 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
1626 source.GetFullPath() );
1630 aRestoredFiles.insert( filename );
1631 cont = tempDir.GetNext( &filename );
1641void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
1642 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
1643 const std::set<wxString>& aRestoredFiles )
1645 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
1649 for(
const wxString& filename : aRestoredFiles )
1651 wxFileName toRemove( aProjectPath, filename );
1652 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
1653 toRemove.GetFullPath() );
1655 if( toRemove.DirExists() )
1657 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1659 else if( toRemove.FileExists() )
1661 wxRemoveFile( toRemove.GetFullPath() );
1666 if( wxDirExists( aBackupPath ) )
1668 for(
const wxString& filename : aBackedUpFiles )
1670 wxFileName source( aBackupPath, filename );
1671 wxFileName dest( aProjectPath, filename );
1673 if( source.Exists() )
1675 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1676 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
1677 dest.GetFullPath() );
1683 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
1684 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
1691bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
1692 const wxString& aHash )
1694 git_time_t t = git_commit_time( aCommit );
1695 wxDateTime dt( (time_t) t );
1696 git_signature* sig =
nullptr;
1698 git_commit* parent =
nullptr;
1701 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
1702 git_commit_lookup( &parent, aRepo, &parent_id );
1705 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1708 const git_commit* constParent = parent;
1709 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
1710 msg.mb_str().data(), aTree, parent ? 1 : 0,
1711 parent ? &constParent :
nullptr );
1714 git_commit_free( parent );
1715 git_signature_free( sig );
1727 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
1730 std::vector<wxString> lockedFiles;
1731 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
1734 for(
const auto& f : lockedFiles )
1735 lockList += wxS(
"\n - ") + f;
1738 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
1744 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
1746 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
1757 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
1768 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
1770 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
1774 git_commit* commit =
nullptr;
1775 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
1777 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
1781 git_tree* tree =
nullptr;
1782 git_commit_tree( &tree, commit );
1785 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
1787 std::vector<wxString> backupFiles;
1790 if( !backupFiles.empty() )
1794 wxS(
"Pre-restore backup" ) ) )
1797 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
1798 git_tree_free( tree );
1799 git_commit_free( commit );
1805 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
1807 if( wxDirExists( tempRestorePath ) )
1808 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1810 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
1813 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
1815 git_tree_free( tree );
1816 git_commit_free( commit );
1820 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
1823 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
1825 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
1826 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1827 git_tree_free( tree );
1828 git_commit_free( commit );
1833 std::set<wxString> restoredFiles;
1834 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
1836 std::vector<wxString> filesToDelete;
1837 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
1839 bool keepAllFiles =
true;
1840 if( !confirmFileDeletion( aParent, filesToDelete, keepAllFiles ) )
1843 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1844 git_tree_free( tree );
1845 git_commit_free( commit );
1850 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
1852 wxString backupPath = aProjectPath + wxS(
"_restore_backup");
1855 if( wxDirExists( backupPath ) )
1857 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Removing old backup %s" ),
1859 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1863 std::set<wxString> backedUpFiles;
1864 std::set<wxString> restoredFilesSet;
1867 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
1870 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1872 git_tree_free( tree );
1873 git_commit_free( commit );
1878 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
1880 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1882 git_tree_free( tree );
1883 git_commit_free( commit );
1888 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Restore successful, cleaning up" ) );
1889 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1890 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1893 recordRestoreInHistory( repo, commit, tree, aHash );
1895 git_tree_free( tree );
1896 git_commit_free( commit );
1898 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
1908 git_repository* repo =
nullptr;
1910 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1913 git_revwalk* walk =
nullptr;
1914 git_revwalk_new( &walk, repo );
1915 git_revwalk_push_head( walk );
1917 std::vector<wxString> choices;
1918 std::vector<wxString> hashes;
1921 while( git_revwalk_next( &oid, walk ) == 0 )
1923 git_commit* commit =
nullptr;
1924 git_commit_lookup( &commit, repo, &oid );
1926 git_time_t t = git_commit_time( commit );
1927 wxDateTime dt( (time_t) t );
1930 line.Printf( wxS(
"%s %s" ), dt.FormatISOCombined().c_str(),
1931 wxString::FromUTF8( git_commit_summary( commit ) ) );
1932 choices.push_back( line );
1933 hashes.push_back( wxString::FromUTF8( git_oid_tostr_s( &oid ) ) );
1934 git_commit_free( commit );
1937 git_revwalk_free( walk );
1938 git_repository_free( repo );
1940 if( choices.empty() )
1943 int index = wxGetSingleChoiceIndex(
_(
"Select snapshot" ),
_(
"Restore" ),
1944 (
int) choices.size(), &choices[0], aParent );
1946 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.
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
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.