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 git_odb* odb =
nullptr;
921 git_repository_odb( &odb, repo );
923 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
926 size_t cnt = git_tree_entrycount( tree );
928 for(
size_t i = 0; i < cnt; ++i )
930 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
932 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
934 const git_oid* bid = git_tree_entry_id( entry );
936 if( seenBlobs.find( *bid ) == seenBlobs.end() )
939 git_object_t type = GIT_OBJECT_ANY;
941 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
944 seenBlobs.insert( *bid );
947 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
949 git_tree* sub =
nullptr;
951 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
953 added += accountTree( sub );
954 git_tree_free( sub );
962 for(
const git_oid& cOid : commits )
964 git_commit* c =
nullptr;
966 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
969 git_tree* tree =
nullptr;
970 git_commit_tree( &tree, c );
971 size_t add = accountTree( tree );
972 git_tree_free( tree );
973 git_commit_free( c );
975 if( keep.empty() || keptBytes + add <= aMaxBytes )
977 keep.push_back( cOid );
987 keep.push_back( commits.front() );
991 std::vector<std::pair<wxString, git_oid>> tagTargets;
992 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
993 [](
const git_oid& a,
const git_oid& b )
995 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
997 git_strarray tagList;
999 if( git_tag_list( &tagList, repo ) == 0 )
1001 for(
size_t i = 0; i < tagList.count; ++i )
1003 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1004 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1006 git_reference* tref =
nullptr;
1008 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1010 const git_oid* toid = git_reference_target( tref );
1014 tagTargets.emplace_back(
name, *toid );
1015 taggedCommits.insert( *toid );
1019 for(
const auto& k : keep )
1021 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1031 keep.push_back( *toid );
1032 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1037 git_reference_free( tref );
1041 git_strarray_free( &tagList );
1045 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1046 wxString trimPath = trimFn.GetPath();
1048 if( wxDirExists( trimPath ) )
1049 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1051 wxMkdir( trimPath );
1052 git_repository* newRepo =
nullptr;
1054 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1058 std::reverse( keep.begin(), keep.end() );
1059 git_commit* parent =
nullptr;
1060 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1061 std::vector<MAP_ENTRY> commitMap;
1063 for(
const git_oid& co : keep )
1065 git_commit* orig =
nullptr;
1067 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1070 git_tree* tree =
nullptr;
1071 git_commit_tree( &tree, orig );
1075 wxArrayString toDelete;
1076 wxDir d( trimPath );
1078 bool cont = d.GetFirst( &nm );
1081 if( nm != wxS(
".git") )
1083 cont = d.GetNext( &nm );
1086 for(
auto& del : toDelete )
1088 wxFileName f( trimPath, del );
1090 wxFileName::Rmdir( f.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1091 else if( f.FileExists() )
1092 wxRemoveFile( f.GetFullPath() );
1096 std::function<void(git_tree*,
const wxString&)> writeTree = [&]( git_tree* t,
const wxString& base )
1098 size_t ecnt = git_tree_entrycount( t );
1099 for(
size_t i = 0; i < ecnt; ++i )
1101 const git_tree_entry* e = git_tree_entry_byindex( t, i );
1102 wxString
name = wxString::FromUTF8( git_tree_entry_name( e ) );
1104 if( git_tree_entry_type( e ) == GIT_OBJECT_TREE )
1106 wxFileName dir( base,
name );
1107 wxMkdir( dir.GetFullPath() );
1108 git_tree* sub =
nullptr;
1110 if( git_tree_lookup( &sub, repo, git_tree_entry_id( e ) ) == 0 )
1112 writeTree( sub, dir.GetFullPath() );
1113 git_tree_free( sub );
1116 else if( git_tree_entry_type( e ) == GIT_OBJECT_BLOB )
1118 git_blob* blob =
nullptr;
1120 if( git_blob_lookup( &blob, repo, git_tree_entry_id( e ) ) == 0 )
1122 wxFileName file( base,
name );
1123 wxFFile f( file.GetFullPath(), wxT(
"wb") );
1127 f.Write( (
const char*) git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1130 git_blob_free( blob );
1136 writeTree( tree, trimPath );
1138 git_index* newIndex =
nullptr;
1139 git_repository_index( &newIndex, newRepo );
1140 git_index_add_all( newIndex,
nullptr, 0,
nullptr,
nullptr );
1141 git_index_write( newIndex );
1143 git_index_write_tree( &newTreeOid, newIndex );
1144 git_tree* newTree =
nullptr;
1145 git_tree_lookup( &newTree, newRepo, &newTreeOid );
1148 const git_signature* origAuthor = git_commit_author( orig );
1149 const git_signature* origCommitter = git_commit_committer( orig );
1150 git_signature* sigAuthor =
nullptr;
1151 git_signature* sigCommitter =
nullptr;
1153 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1154 origAuthor->when.time, origAuthor->when.offset );
1155 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1156 origCommitter->when.time, origCommitter->when.offset );
1158 const git_commit* parents[1];
1159 int parentCount = 0;
1163 parents[0] = parent;
1167 git_oid newCommitOid;
1168 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1169 newTree, parentCount, parentCount ? parents :
nullptr );
1171 git_commit_free( parent );
1173 git_commit_lookup( &parent, newRepo, &newCommitOid );
1175 commitMap.emplace_back( co, newCommitOid );
1177 git_signature_free( sigAuthor );
1178 git_signature_free( sigCommitter );
1179 git_tree_free( newTree );
1180 git_index_free( newIndex );
1181 git_tree_free( tree );
1182 git_commit_free( orig );
1186 git_commit_free( parent );
1189 for(
const auto& tt : tagTargets )
1192 const git_oid* newOid =
nullptr;
1194 for(
const auto& m : commitMap )
1196 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1206 git_object* obj =
nullptr;
1208 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1210 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1211 git_object_free( obj );
1218 git_repository_free( newRepo );
1221 wxString backupOld = hist + wxS(
"_old");
1222 wxRenameFile( hist, backupOld );
1223 wxRenameFile( trimPath, hist );
1224 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1231 git_repository* repo =
nullptr;
1233 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1234 return wxEmptyString;
1237 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1239 git_repository_free( repo );
1240 return wxEmptyString;
1243 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1244 git_repository_free( repo );
1256bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1258 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
1260 wxDir dir( dirPath );
1261 if( !dir.IsOpened() )
1265 bool cont = dir.GetFirst( &filename );
1269 wxFileName fullPath( dirPath, filename );
1272 if( filename == wxS(
".history") || filename == wxS(
".git") )
1274 cont = dir.GetNext( &filename );
1278 if( fullPath.DirExists() )
1280 findLocks( fullPath.GetFullPath() );
1282 else if( fullPath.FileExists()
1289 baseName = baseName.BeforeLast(
'.' );
1290 wxFileName originalFile( dirPath, baseName );
1293 LOCKFILE testLock( originalFile.GetFullPath() );
1294 if( testLock.Valid() && !testLock.IsLockedByMe() )
1296 aLockedFiles.push_back( fullPath.GetFullPath() );
1300 cont = dir.GetNext( &filename );
1304 findLocks( aProjectPath );
1305 return aLockedFiles.empty();
1312bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
1314 bool extractSuccess =
true;
1316 std::function<void( git_tree*,
const wxString& )> extractTree =
1317 [&]( git_tree* t,
const wxString& prefix )
1319 if( !extractSuccess )
1322 size_t cnt = git_tree_entrycount( t );
1323 for(
size_t i = 0; i < cnt; ++i )
1325 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1326 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1327 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
1329 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1331 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
1333 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1336 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
1337 dirPath.GetPath() );
1338 extractSuccess =
false;
1342 git_tree* sub =
nullptr;
1343 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
1345 extractTree( sub, fullPath );
1346 git_tree_free( sub );
1349 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1351 git_blob* blob =
nullptr;
1352 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
1354 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
1356 wxFileName dstDir( dst );
1357 dstDir.SetFullName( wxEmptyString );
1358 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1360 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
1363 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1369 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
1370 dst.GetFullPath() );
1371 extractSuccess =
false;
1372 git_blob_free( blob );
1376 git_blob_free( blob );
1382 extractTree( aTree, wxEmptyString );
1383 return extractSuccess;
1390void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
1391 std::set<wxString>& aFiles )
1393 wxDir dir( aSearchPath );
1394 if( !dir.IsOpened() )
1398 bool cont = dir.GetFirst( &filename );
1402 wxFileName fullPath( aSearchPath, filename );
1403 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
1405 if( fullPath.IsDir() && fullPath.DirExists() )
1407 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
1409 else if( fullPath.FileExists() )
1411 aFiles.insert( relativePath );
1414 cont = dir.GetNext( &filename );
1422bool shouldExcludeFromBackup(
const wxString& aFilename )
1425 return aFilename == wxS(
"fp-info-cache" );
1432void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
1433 std::vector<wxString>& aFilesToDelete )
1435 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
1436 [&](
const wxString& dirPath,
const wxString& relativeBase )
1438 wxDir dir( dirPath );
1439 if( !dir.IsOpened() )
1443 bool cont = dir.GetFirst( &filename );
1448 if( filename == wxS(
".history") || filename == wxS(
".git") ||
1449 filename == wxS(
"_restore_backup") || filename == wxS(
"_restore_temp") )
1451 cont = dir.GetNext( &filename );
1455 wxFileName fullPath( dirPath, filename );
1456 wxString relativePath = relativeBase.IsEmpty() ? filename :
1457 relativeBase + wxS(
"/") + filename;
1459 if( fullPath.IsDir() && fullPath.DirExists() )
1461 scanDirectory( fullPath.GetFullPath(), relativePath );
1463 else if( fullPath.FileExists() )
1466 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
1469 if( !shouldExcludeFromBackup( filename ) )
1470 aFilesToDelete.push_back( relativePath );
1474 cont = dir.GetNext( &filename );
1478 scanDirectory( aProjectPath, wxEmptyString );
1486bool confirmFileDeletion( wxWindow* aParent,
const std::vector<wxString>& aFilesToDelete,
1487 bool& aKeepAllFiles )
1489 if( aFilesToDelete.empty() || !aParent )
1491 aKeepAllFiles =
false;
1495 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
1498 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
1499 for(
size_t i = 0; i < displayCount; ++i )
1501 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
1504 if( aFilesToDelete.size() > displayCount )
1506 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
1507 aFilesToDelete.size() - displayCount );
1511 wxYES_NO | wxCANCEL | wxICON_QUESTION );
1512 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
1513 dlg.SetExtendedMessage(
1514 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
1515 "files in the project directory. Choosing 'Proceed' will delete files that are not "
1516 "present in the restored commit." ) );
1518 int choice = dlg.ShowModal();
1520 if( choice == wxID_CANCEL )
1522 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
1525 else if( choice == wxID_NO )
1527 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
1528 aKeepAllFiles =
true;
1532 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
1533 aKeepAllFiles =
false;
1543bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
1544 const wxString& aTempRestorePath,
bool aKeepAllFiles,
1545 std::set<wxString>& aBackedUpFiles )
1547 wxDir currentDir( aProjectPath );
1548 if( !currentDir.IsOpened() )
1552 bool cont = currentDir.GetFirst( &filename );
1556 if( filename != wxS(
".history" ) && filename != wxS(
".git" ) &&
1557 filename != wxS(
"_restore_backup" ) && filename != wxS(
"_restore_temp" ) )
1560 bool shouldBackup = !aKeepAllFiles;
1565 wxFileName testPath( aTempRestorePath, filename );
1566 shouldBackup = testPath.Exists();
1571 wxFileName source( aProjectPath, filename );
1572 wxFileName dest( aBackupPath, filename );
1575 if( !wxDirExists( aBackupPath ) )
1578 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
1580 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
1584 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
1585 source.GetFullPath(), dest.GetFullPath() );
1587 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1590 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
1591 source.GetFullPath() );
1595 aBackedUpFiles.insert( filename );
1598 cont = currentDir.GetNext( &filename );
1608bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
1609 std::set<wxString>& aRestoredFiles )
1611 wxDir tempDir( aTempRestorePath );
1612 if( !tempDir.IsOpened() )
1616 bool cont = tempDir.GetFirst( &filename );
1620 wxFileName source( aTempRestorePath, filename );
1621 wxFileName dest( aProjectPath, filename );
1624 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
1625 source.GetFullPath(), dest.GetFullPath() );
1627 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1630 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
1631 source.GetFullPath() );
1635 aRestoredFiles.insert( filename );
1636 cont = tempDir.GetNext( &filename );
1646void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
1647 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
1648 const std::set<wxString>& aRestoredFiles )
1650 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
1654 for(
const wxString& filename : aRestoredFiles )
1656 wxFileName toRemove( aProjectPath, filename );
1657 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
1658 toRemove.GetFullPath() );
1660 if( toRemove.DirExists() )
1662 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1664 else if( toRemove.FileExists() )
1666 wxRemoveFile( toRemove.GetFullPath() );
1671 if( wxDirExists( aBackupPath ) )
1673 for(
const wxString& filename : aBackedUpFiles )
1675 wxFileName source( aBackupPath, filename );
1676 wxFileName dest( aProjectPath, filename );
1678 if( source.Exists() )
1680 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1681 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
1682 dest.GetFullPath() );
1688 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
1689 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
1696bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
1697 const wxString& aHash )
1699 git_time_t t = git_commit_time( aCommit );
1700 wxDateTime dt( (time_t) t );
1701 git_signature* sig =
nullptr;
1703 git_commit* parent =
nullptr;
1706 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
1707 git_commit_lookup( &parent, aRepo, &parent_id );
1710 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1713 const git_commit* constParent = parent;
1714 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
1715 msg.mb_str().data(), aTree, parent ? 1 : 0,
1716 parent ? &constParent :
nullptr );
1719 git_commit_free( parent );
1720 git_signature_free( sig );
1732 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
1735 std::vector<wxString> lockedFiles;
1736 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
1739 for(
const auto& f : lockedFiles )
1740 lockList += wxS(
"\n - ") + f;
1743 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
1749 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
1751 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
1762 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
1773 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
1775 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
1779 git_commit* commit =
nullptr;
1780 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
1782 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
1786 git_tree* tree =
nullptr;
1787 git_commit_tree( &tree, commit );
1790 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
1792 std::vector<wxString> backupFiles;
1795 if( !backupFiles.empty() )
1799 wxS(
"Pre-restore backup" ) ) )
1802 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
1803 git_tree_free( tree );
1804 git_commit_free( commit );
1810 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
1812 if( wxDirExists( tempRestorePath ) )
1813 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1815 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
1818 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
1820 git_tree_free( tree );
1821 git_commit_free( commit );
1825 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
1828 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
1830 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
1831 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1832 git_tree_free( tree );
1833 git_commit_free( commit );
1838 std::set<wxString> restoredFiles;
1839 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
1841 std::vector<wxString> filesToDelete;
1842 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
1844 bool keepAllFiles =
true;
1845 if( !confirmFileDeletion( aParent, filesToDelete, keepAllFiles ) )
1848 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1849 git_tree_free( tree );
1850 git_commit_free( commit );
1855 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
1857 wxString backupPath = aProjectPath + wxS(
"_restore_backup");
1860 if( wxDirExists( backupPath ) )
1862 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Removing old backup %s" ),
1864 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1868 std::set<wxString> backedUpFiles;
1869 std::set<wxString> restoredFilesSet;
1872 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
1875 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1877 git_tree_free( tree );
1878 git_commit_free( commit );
1883 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
1885 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1887 git_tree_free( tree );
1888 git_commit_free( commit );
1893 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Restore successful, cleaning up" ) );
1894 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1895 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1898 recordRestoreInHistory( repo, commit, tree, aHash );
1900 git_tree_free( tree );
1901 git_commit_free( commit );
1903 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
1913 git_repository* repo =
nullptr;
1915 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1918 git_revwalk* walk =
nullptr;
1919 git_revwalk_new( &walk, repo );
1920 git_revwalk_push_head( walk );
1922 std::vector<wxString> choices;
1923 std::vector<wxString> hashes;
1926 while( git_revwalk_next( &oid, walk ) == 0 )
1928 git_commit* commit =
nullptr;
1929 git_commit_lookup( &commit, repo, &oid );
1931 git_time_t t = git_commit_time( commit );
1932 wxDateTime dt( (time_t) t );
1935 line.Printf( wxS(
"%s %s" ), dt.FormatISOCombined().c_str(),
1936 wxString::FromUTF8( git_commit_summary( commit ) ) );
1937 choices.push_back( line );
1938 hashes.push_back( wxString::FromUTF8( git_oid_tostr_s( &oid ) ) );
1939 git_commit_free( commit );
1942 git_revwalk_free( walk );
1943 git_repository_free( repo );
1945 if( choices.empty() )
1948 int index = wxGetSingleChoiceIndex(
_(
"Select snapshot" ),
_(
"Restore" ),
1949 (
int) choices.size(), &choices[0], aParent );
1951 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.