39#include <wx/filename.h>
43#include <wx/datetime.h>
58 wxFileName p( aProjectPath, wxEmptyString );
59 p.AppendDir( wxS(
".history" ) );
74 wxFileName fn( aFile );
76 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
84 const void* aSaverObject,
85 const std::function<
void(
const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
89 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping"), aSaverObject );
94 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
102 auto it =
m_savers.find( aSaverObject );
107 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
116 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers") );
122 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
124 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
128 Init( aProjectPath );
130 wxLogTrace(
traceAutoSave, wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu"),
131 aProjectPath, aTitle,
m_savers.size() );
135 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
142 wxLogTrace(
traceAutoSave, wxS(
"[history] previous save still in progress; skipping cycle") );
147 std::vector<HISTORY_FILE_DATA> fileData;
149 for(
const auto& [saverObject, saver] :
m_savers )
151 size_t before = fileData.size();
152 saver( aProjectPath, fileData );
153 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p produced %zu entries (total=%zu)"),
154 saverObject, fileData.size() - before, fileData.size() );
158 wxString projectDir = aProjectPath;
159 if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
160 projectDir += wxFileName::GetPathSeparator();
162 auto it = std::remove_if( fileData.begin(), fileData.end(),
165 if( !entry.path.StartsWith( projectDir ) )
167 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out entry outside project: %s"), entry.path );
172 fileData.erase( it, fileData.end() );
174 if( fileData.empty() )
176 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no entries; skipping") );
181 m_saveInProgress.store(
true, std::memory_order_release );
184 [
this, projectPath = aProjectPath, title = aTitle,
185 data = std::move( fileData )]()
mutable ->
bool
187 bool result = commitInBackground( projectPath, title, data );
188 m_saveInProgress.store(
false, std::memory_order_release );
197 const std::vector<HISTORY_FILE_DATA>& aFileData )
199 wxLogTrace(
traceAutoSave, wxS(
"[history] background: writing %zu entries for '%s'"),
200 aFileData.size(), aProjectPath );
207 if( !entry.content.empty() )
209 std::string buf = entry.content;
214 wxFFile fp( entry.path, wxS(
"wb" ) );
218 fp.Write( buf.data(), buf.size() );
220 wxLogTrace(
traceAutoSave, wxS(
"[history] background: wrote %zu bytes to '%s'"),
221 buf.size(), entry.path );
225 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to open '%s' for writing"),
229 else if( !entry.sourcePath.IsEmpty() )
231 wxCopyFile( entry.sourcePath, entry.path,
true );
232 wxLogTrace(
traceAutoSave, wxS(
"[history] background: copied '%s' -> '%s'"),
233 entry.sourcePath, entry.path );
249 git_repository_set_workdir( repo, hist.mb_str().data(),
false );
254 wxFileName src( entry.path );
256 if( !src.FileExists() )
259 if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
261 std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
262 git_index_add_bypath(
index, relHist.c_str() );
268 git_commit* head_commit =
nullptr;
269 git_tree* head_tree =
nullptr;
271 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
272 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
273 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
275 git_tree* rawIndexTree =
nullptr;
276 git_oid index_tree_oid;
278 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
280 if( head_tree ) git_tree_free( head_tree );
281 if( head_commit ) git_commit_free( head_commit );
282 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to write index tree" ) );
286 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
287 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
289 bool hasChanges =
true;
293 git_diff* diff =
nullptr;
295 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
297 hasChanges = git_diff_num_deltas( diff ) > 0;
298 wxLogTrace(
traceAutoSave, wxS(
"[history] background: diff deltas=%u"), (
unsigned) git_diff_num_deltas( diff ) );
299 git_diff_free( diff );
303 if( head_tree ) git_tree_free( head_tree );
304 if( head_commit ) git_commit_free( head_commit );
308 wxLogTrace(
traceAutoSave, wxS(
"[history] background: no changes detected; no commit") );
312 git_signature* rawSig =
nullptr;
314 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
316 git_commit* parent =
nullptr;
320 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
322 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
326 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
328 const git_commit* constParent = parent;
330 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
331 msg.mb_str().data(), indexTree.get(), parents,
332 parents ? &constParent :
nullptr );
335 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit created %s (%s entries=%zu)"),
336 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
338 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit failed rc=%d"), rc );
340 if( parent ) git_commit_free( parent );
342 git_index_write(
index );
351 wxLogTrace(
traceAutoSave, wxS(
"[history] waiting for pending background save") );
367 if( aProjectPath.IsEmpty() )
370 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
375 if( !wxDirExists( hist ) )
377 if( wxIsWritable( aProjectPath ) )
379 if( !wxMkdir( hist ) )
386 git_repository* rawRepo =
nullptr;
387 bool isNewRepo =
false;
389 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
391 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
396 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
397 if( !ignoreFile.FileExists() )
399 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
402 f.Write( wxS(
"fp-info-cache\n*-backups\nREADME.txt\n" ) );
407 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
409 if( !readmeFile.FileExists() )
411 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
415 f.Write( wxS(
"KiCad Local History Directory\n"
416 "=============================\n\n"
417 "This directory contains automatic snapshots of your project files.\n"
418 "KiCad periodically saves copies of your work here, allowing you to\n"
419 "recover from accidental changes or data loss.\n\n"
420 "You can browse and restore previous versions through KiCad's\n"
421 "File > Local History menu.\n\n"
422 "To disable this feature:\n"
423 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
424 "This directory can be safely deleted if you no longer need the\n"
425 "history, but doing so will permanently remove all saved snapshots.\n" ) );
431 git_repository_free( rawRepo );
437 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: New repository created, collecting existing files" ) );
441 std::function<void(
const wxString& )> collect = [&](
const wxString&
path )
449 bool cont = d.GetFirst( &
name );
454 if(
name.StartsWith( wxS(
"." ) ) ||
name.EndsWith( wxS(
"-backups" ) ) )
456 cont = d.GetNext( &
name );
461 wxString fullPath = fn.GetFullPath();
463 if( wxFileName::DirExists( fullPath ) )
467 else if( fn.FileExists() )
470 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
471 files.Add( fn.GetFullPath() );
474 cont = d.GetNext( &
name );
478 collect( aProjectPath );
480 if( files.GetCount() > 0 )
482 std::vector<wxString> vec;
483 vec.reserve( files.GetCount() );
485 for(
unsigned i = 0; i < files.GetCount(); ++i )
486 vec.push_back( files[i] );
488 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: Creating initial snapshot with %zu files" ), vec.size() );
493 TagSave( aProjectPath, wxS(
"project" ) );
497 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: No files found to add to initial snapshot" ) );
515 const wxString& aHistoryPath,
const wxString& aProjectPath,
516 const std::vector<wxString>& aFiles,
const wxString& aTitle )
518 std::vector<std::string> filesArrStr;
520 for(
const wxString& file : aFiles )
522 wxFileName src( file );
525 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
526 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
528 relPath = src.GetFullName();
530 relPath.Replace(
"\\",
"/" );
531 std::string relPathStr = relPath.ToStdString();
533 unsigned int status = 0;
534 int rc = git_status_file( &status, repo, relPathStr.data() );
536 if( rc == 0 && status != 0 )
538 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
539 filesArrStr.emplace_back( relPathStr );
543 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
544 filesArrStr.emplace_back( relPathStr );
548 std::vector<char*> cStrings( filesArrStr.size() );
550 for(
size_t i = 0; i < filesArrStr.size(); i++ )
551 cStrings[i] = filesArrStr[i].data();
553 git_strarray filesArrGit;
554 filesArrGit.count = filesArrStr.size();
555 filesArrGit.strings = cStrings.data();
557 if( filesArrStr.size() == 0 )
563 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
565 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
571 if( git_index_write_tree( &tree_id,
index ) != 0 )
574 git_tree* rawTree =
nullptr;
575 git_tree_lookup( &rawTree, repo, &tree_id );
576 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
578 git_signature* rawSig =
nullptr;
580 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
581 &git_signature_free );
583 git_commit* rawParent =
nullptr;
587 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
589 git_commit_lookup( &rawParent, repo, &parent_id );
593 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
596 git_tree* rawParentTree =
nullptr;
599 git_commit_tree( &rawParentTree, parent.get() );
601 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
603 git_diff* rawDiff =
nullptr;
604 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
605 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
607 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
609 if( numChangedFiles == 0 )
611 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
617 if( !aTitle.IsEmpty() )
618 msg << aTitle << wxS(
": " );
620 msg << numChangedFiles << wxS(
" files changed" );
622 for(
size_t i = 0; i < numChangedFiles; ++i )
624 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
625 git_patch* rawPatch =
nullptr;
626 git_patch_from_diff( &rawPatch, diff.get(), i );
627 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
629 size_t context = 0, adds = 0, dels = 0;
630 git_patch_line_stats( &context, &adds, &dels, patch.get() );
631 size_t updated = std::min( adds, dels );
634 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
635 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
639 git_commit* parentPtr = parent.get();
640 const git_commit* constParentPtr = parentPtr;
641 if( git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr, msg.mb_str().data(), tree.get(),
642 parents, parentPtr ? &constParentPtr :
nullptr )
648 git_index_write(
index );
655 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled )
658 wxString proj = wxFileName( aFiles[0] ).GetPath();
669 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitSnapshot failed to acquire lock: %s"),
684 wxDir dir( aProjectPath );
686 if( !dir.IsOpened() )
690 std::function<void(
const wxString&)> collect = [&](
const wxString&
path )
698 bool cont = d.GetFirst( &
name );
702 if(
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) )
704 cont = d.GetNext( &
name );
709 wxString fullPath = fn.GetFullPath();
711 if( wxFileName::DirExists( fullPath ) )
715 else if( fn.FileExists() )
718 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
719 aFiles.push_back( fn.GetFullPath() );
722 cont = d.GetNext( &
name );
726 collect( aProjectPath );
732 std::vector<wxString> files;
748 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
755 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
765 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
770 git_reference* ref =
nullptr;
773 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
774 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
777 git_object* head_obj =
nullptr;
778 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
779 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
780 git_object_free( head_obj );
783 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
784 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
786 git_reference_delete( ref );
787 git_reference_free( ref );
790 git_oid last_tag_oid;
791 git_object* head_obj2 =
nullptr;
792 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
793 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
794 git_object_free( head_obj2 );
802 git_repository* repo =
nullptr;
804 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
808 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
810 git_repository_free( repo );
814 git_commit* head_commit =
nullptr;
815 git_commit_lookup( &head_commit, repo, &head_oid );
816 git_time_t head_time = git_commit_time( head_commit );
819 git_tag_list_match( &tags,
"Last_Save_*", repo );
820 git_time_t save_time = 0;
822 for(
size_t i = 0; i < tags.count; ++i )
824 git_reference* ref =
nullptr;
825 if( git_reference_lookup( &ref, repo,
826 ( wxS(
"refs/tags/" ) +
827 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
829 const git_oid* oid = git_reference_target( ref );
830 git_commit* c =
nullptr;
831 if( git_commit_lookup( &c, repo, oid ) == 0 )
833 git_time_t t = git_commit_time( c );
836 git_commit_free( c );
838 git_reference_free( ref );
842 git_strarray_free( &tags );
843 git_commit_free( head_commit );
844 git_repository_free( repo );
851 return head_time > save_time;
855 const wxString& aMessage )
857 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
864 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
873 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
874 git_reference* lastRef =
nullptr;
875 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
877 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
879 const git_oid* lastOid = git_reference_target( lastRef );
880 git_commit* lastCommit =
nullptr;
881 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
883 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
885 git_tree* lastTree =
nullptr;
886 git_commit_tree( &lastTree, lastCommit );
887 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
891 git_commit* headCommit =
nullptr;
893 const git_commit* parentArray[1];
894 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
895 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
897 parentArray[0] = headCommit;
901 git_signature* sigRaw =
nullptr;
903 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
905 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
906 git_oid newCommitOid;
907 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
908 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
909 if( headCommit ) git_commit_free( headCommit );
914 git_reference* existing =
nullptr;
915 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
917 git_reference_delete( existing );
918 git_reference_free( existing );
920 git_object* newCommitObj =
nullptr;
921 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
923 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
924 git_object_free( newCommitObj );
933 if( !dir.IsOpened() )
936 bool cont = dir.GetFirst( &
name );
940 wxString fullPath = fn.GetFullPath();
942 if( wxFileName::DirExists( fullPath ) )
944 else if( fn.FileExists() )
945 total += (size_t) fn.GetSize().GetValue();
946 cont = dir.GetNext( &
name );
952static bool copyTreeObjects( git_repository* aSrcRepo, git_odb* aSrcOdb, git_odb* aDstOdb,
const git_oid* aTreeOid,
953 std::set<git_oid,
bool ( * )(
const git_oid&,
const git_oid& )>& aCopied )
955 if( aCopied.count( *aTreeOid ) )
958 git_odb_object* obj =
nullptr;
960 if( git_odb_read( &obj, aSrcOdb, aTreeOid ) != 0 )
964 int err = git_odb_write( &written, aDstOdb, git_odb_object_data( obj ), git_odb_object_size( obj ),
965 git_odb_object_type( obj ) );
966 git_odb_object_free( obj );
971 aCopied.insert( *aTreeOid );
973 git_tree* tree =
nullptr;
975 if( git_tree_lookup( &tree, aSrcRepo, aTreeOid ) != 0 )
978 size_t cnt = git_tree_entrycount( tree );
980 for(
size_t i = 0; i < cnt; ++i )
982 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
983 const git_oid* entryId = git_tree_entry_id( entry );
985 if( aCopied.count( *entryId ) )
988 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
992 git_tree_free( tree );
996 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
998 git_odb_object* blobObj =
nullptr;
1000 if( git_odb_read( &blobObj, aSrcOdb, entryId ) == 0 )
1002 git_oid blobWritten;
1004 if( git_odb_write( &blobWritten, aDstOdb, git_odb_object_data( blobObj ),
1005 git_odb_object_size( blobObj ), git_odb_object_type( blobObj ) )
1008 git_odb_object_free( blobObj );
1009 git_tree_free( tree );
1013 git_odb_object_free( blobObj );
1014 aCopied.insert( *entryId );
1019 git_tree_free( tree );
1028 git_packbuilder* pb =
nullptr;
1030 if( git_packbuilder_new( &pb, aRepo ) != 0 )
1033 git_revwalk* walk =
nullptr;
1035 if( git_revwalk_new( &walk, aRepo ) != 0 )
1037 git_packbuilder_free( pb );
1041 git_revwalk_push_head( walk );
1044 while( git_revwalk_next( &oid, walk ) == 0 )
1046 if( git_packbuilder_insert_commit( pb, &oid ) != 0 )
1048 git_revwalk_free( walk );
1049 git_packbuilder_free( pb );
1054 git_revwalk_free( walk );
1058 git_packbuilder_set_callbacks(
1060 [](
int aStage, uint32_t aCurrent, uint32_t aTotal,
void* aPayload )
1067 reporter->KeepRefreshing();
1073 if( git_packbuilder_write( pb,
nullptr, 0,
nullptr,
nullptr ) != 0 )
1075 git_packbuilder_free( pb );
1079 git_packbuilder_free( pb );
1081 wxString objPath = wxString::FromUTF8( git_repository_path( aRepo ) ) + wxS(
"objects" );
1082 wxDir objDir( objPath );
1084 if( objDir.IsOpened() )
1086 wxArrayString toRemove;
1088 bool cont = objDir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS );
1092 if(
name.length() == 2 )
1093 toRemove.Add( objPath + wxFileName::GetPathSeparator() +
name );
1095 cont = objDir.GetNext( &
name );
1098 for(
const wxString& dir : toRemove )
1099 wxFileName::Rmdir( dir, wxPATH_RMDIR_RECURSIVE );
1108 if( aMaxBytes == 0 )
1113 if( !wxDirExists( hist ) )
1118 if( current <= aMaxBytes )
1125 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
1135 aReporter->
Report(
_(
"Compacting local history..." ) );
1142 if( current <= aMaxBytes )
1146 git_revwalk* walk =
nullptr;
1147 git_revwalk_new( &walk, repo );
1148 git_revwalk_sorting( walk, GIT_SORT_TIME );
1149 git_revwalk_push_head( walk );
1150 std::vector<git_oid> commits;
1153 while( git_revwalk_next( &oid, walk ) == 0 )
1154 commits.push_back( oid );
1156 git_revwalk_free( walk );
1158 if( commits.empty() )
1162 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
1163 [](
const git_oid& a,
const git_oid& b )
1165 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1168 size_t keptBytes = 0;
1169 std::vector<git_oid> keep;
1171 git_odb* odb =
nullptr;
1172 git_repository_odb( &odb, repo );
1174 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
1177 size_t cnt = git_tree_entrycount( tree );
1179 for(
size_t i = 0; i < cnt; ++i )
1181 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1183 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1185 const git_oid* bid = git_tree_entry_id( entry );
1187 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1190 git_object_t type = GIT_OBJECT_ANY;
1192 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1195 seenBlobs.insert( *bid );
1198 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1200 git_tree* sub =
nullptr;
1202 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1204 added += accountTree( sub );
1205 git_tree_free( sub );
1213 for(
const git_oid& cOid : commits )
1215 git_commit* c =
nullptr;
1217 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1220 git_tree* tree =
nullptr;
1221 git_commit_tree( &tree, c );
1222 size_t add = accountTree( tree );
1223 git_tree_free( tree );
1224 git_commit_free( c );
1226 if( keep.empty() || keptBytes + add <= aMaxBytes )
1228 keep.push_back( cOid );
1236 keep.push_back( commits.front() );
1240 std::vector<std::pair<wxString, git_oid>> tagTargets;
1241 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
1242 [](
const git_oid& a,
const git_oid& b )
1244 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1246 git_strarray tagList;
1248 if( git_tag_list( &tagList, repo ) == 0 )
1250 for(
size_t i = 0; i < tagList.count; ++i )
1252 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1253 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1255 git_reference* tref =
nullptr;
1257 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1259 const git_oid* toid = git_reference_target( tref );
1263 tagTargets.emplace_back(
name, *toid );
1264 taggedCommits.insert( *toid );
1268 for(
const auto& k : keep )
1270 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1280 keep.push_back( *toid );
1281 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1286 git_reference_free( tref );
1290 git_strarray_free( &tagList );
1294 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1295 wxString trimPath = trimFn.GetPath();
1297 if( wxDirExists( trimPath ) )
1298 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1300 wxMkdir( trimPath );
1301 git_repository* newRepo =
nullptr;
1303 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1305 git_odb_free( odb );
1309 git_odb* dstOdb =
nullptr;
1311 if( git_repository_odb( &dstOdb, newRepo ) != 0 )
1313 git_repository_free( newRepo );
1314 git_odb_free( odb );
1318 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> copiedObjects(
1319 [](
const git_oid& a,
const git_oid& b )
1321 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1325 std::reverse( keep.begin(), keep.end() );
1326 git_commit* parent =
nullptr;
1327 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1328 std::vector<MAP_ENTRY> commitMap;
1332 aReporter->
AdvancePhase(
_(
"Trimming local history..." ) );
1336 for(
size_t idx = 0; idx < keep.size(); ++idx )
1341 const git_oid& co = keep[idx];
1342 git_commit* orig =
nullptr;
1344 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1347 git_tree* tree =
nullptr;
1348 git_commit_tree( &tree, orig );
1350 copyTreeObjects( repo, odb, dstOdb, git_tree_id( tree ), copiedObjects );
1352 git_tree* newTree =
nullptr;
1353 git_tree_lookup( &newTree, newRepo, git_tree_id( tree ) );
1355 git_tree_free( tree );
1358 const git_signature* origAuthor = git_commit_author( orig );
1359 const git_signature* origCommitter = git_commit_committer( orig );
1360 git_signature* sigAuthor =
nullptr;
1361 git_signature* sigCommitter =
nullptr;
1363 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1364 origAuthor->when.time, origAuthor->when.offset );
1365 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1366 origCommitter->when.time, origCommitter->when.offset );
1368 const git_commit* parents[1];
1369 int parentCount = 0;
1373 parents[0] = parent;
1377 git_oid newCommitOid;
1378 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1379 newTree, parentCount, parentCount ? parents :
nullptr );
1382 git_commit_free( parent );
1384 git_commit_lookup( &parent, newRepo, &newCommitOid );
1386 commitMap.emplace_back( co, newCommitOid );
1388 git_signature_free( sigAuthor );
1389 git_signature_free( sigCommitter );
1390 git_tree_free( newTree );
1391 git_commit_free( orig );
1395 git_commit_free( parent );
1398 for(
const auto& tt : tagTargets )
1401 const git_oid* newOid =
nullptr;
1403 for(
const auto& m : commitMap )
1405 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1415 git_object* obj =
nullptr;
1417 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1419 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1420 git_object_free( obj );
1425 aReporter->
AdvancePhase(
_(
"Compacting trimmed history..." ) );
1432 git_odb_free( dstOdb );
1433 git_odb_free( odb );
1434 git_repository_free( newRepo );
1439 wxString backupOld = hist + wxS(
"_old");
1440 wxRenameFile( hist, backupOld );
1441 wxRenameFile( trimPath, hist );
1442 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1449 git_repository* repo =
nullptr;
1451 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1452 return wxEmptyString;
1455 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1457 git_repository_free( repo );
1458 return wxEmptyString;
1461 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1462 git_repository_free( repo );
1474bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1476 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
1478 wxDir dir( dirPath );
1479 if( !dir.IsOpened() )
1483 bool cont = dir.GetFirst( &filename );
1487 wxFileName fullPath( dirPath, filename );
1490 if( filename == wxS(
".history") || filename == wxS(
".git") )
1492 cont = dir.GetNext( &filename );
1496 if( fullPath.DirExists() )
1498 findLocks( fullPath.GetFullPath() );
1500 else if( fullPath.FileExists()
1507 baseName = baseName.BeforeLast(
'.' );
1508 wxFileName originalFile( dirPath, baseName );
1511 LOCKFILE testLock( originalFile.GetFullPath() );
1512 if( testLock.Valid() && !testLock.IsLockedByMe() )
1514 aLockedFiles.push_back( fullPath.GetFullPath() );
1518 cont = dir.GetNext( &filename );
1522 findLocks( aProjectPath );
1523 return aLockedFiles.empty();
1530bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
1532 bool extractSuccess =
true;
1534 std::function<void( git_tree*,
const wxString& )> extractTree =
1535 [&]( git_tree* t,
const wxString& prefix )
1537 if( !extractSuccess )
1540 size_t cnt = git_tree_entrycount( t );
1541 for(
size_t i = 0; i < cnt; ++i )
1543 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1544 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1545 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
1547 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1549 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
1551 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1554 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
1555 dirPath.GetPath() );
1556 extractSuccess =
false;
1560 git_tree* sub =
nullptr;
1561 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
1563 extractTree( sub, fullPath );
1564 git_tree_free( sub );
1567 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1569 git_blob* blob =
nullptr;
1570 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
1572 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
1574 wxFileName dstDir( dst );
1575 dstDir.SetFullName( wxEmptyString );
1576 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1578 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
1581 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1587 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
1588 dst.GetFullPath() );
1589 extractSuccess =
false;
1590 git_blob_free( blob );
1594 git_blob_free( blob );
1600 extractTree( aTree, wxEmptyString );
1601 return extractSuccess;
1608void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
1609 std::set<wxString>& aFiles )
1611 wxDir dir( aSearchPath );
1612 if( !dir.IsOpened() )
1616 bool cont = dir.GetFirst( &filename );
1620 wxFileName fullPath( aSearchPath, filename );
1621 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
1623 if( fullPath.IsDir() && fullPath.DirExists() )
1625 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
1627 else if( fullPath.FileExists() )
1629 aFiles.insert( relativePath );
1632 cont = dir.GetNext( &filename );
1642bool isRestoreProtectedEntry(
const wxString& aName )
1644 return aName == wxS(
".history" )
1645 || aName == wxS(
".git" )
1646 || aName == wxS(
"_restore_backup" )
1647 || aName == wxS(
"_restore_temp" )
1655bool shouldExcludeFromBackup(
const wxString& aFilename )
1658 return aFilename == wxS(
"fp-info-cache" ) || isRestoreProtectedEntry( aFilename );
1665void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
1666 std::vector<wxString>& aFilesToDelete )
1668 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
1669 [&](
const wxString& dirPath,
const wxString& relativeBase )
1671 wxDir dir( dirPath );
1672 if( !dir.IsOpened() )
1676 bool cont = dir.GetFirst( &filename );
1682 if( relativeBase.IsEmpty() && isRestoreProtectedEntry( filename ) )
1684 cont = dir.GetNext( &filename );
1688 wxFileName fullPath( dirPath, filename );
1689 wxString relativePath = relativeBase.IsEmpty() ? filename :
1690 relativeBase + wxS(
"/") + filename;
1692 if( fullPath.IsDir() && fullPath.DirExists() )
1694 scanDirectory( fullPath.GetFullPath(), relativePath );
1696 else if( fullPath.FileExists() )
1699 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
1702 if( !shouldExcludeFromBackup( filename ) )
1703 aFilesToDelete.push_back( relativePath );
1707 cont = dir.GetNext( &filename );
1711 scanDirectory( aProjectPath, wxEmptyString );
1719bool confirmFileDeletion( wxWindow* aParent,
const std::vector<wxString>& aFilesToDelete,
1720 bool& aKeepAllFiles )
1722 if( aFilesToDelete.empty() || !aParent )
1724 aKeepAllFiles =
false;
1728 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
1731 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
1732 for(
size_t i = 0; i < displayCount; ++i )
1734 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
1737 if( aFilesToDelete.size() > displayCount )
1739 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
1740 aFilesToDelete.size() - displayCount );
1744 wxYES_NO | wxCANCEL | wxICON_QUESTION );
1745 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
1746 dlg.SetExtendedMessage(
1747 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
1748 "files in the project directory. Choosing 'Proceed' will delete files that are not "
1749 "present in the restored commit." ) );
1751 int choice = dlg.ShowModal();
1753 if( choice == wxID_CANCEL )
1755 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
1758 else if( choice == wxID_NO )
1760 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
1761 aKeepAllFiles =
true;
1765 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
1766 aKeepAllFiles =
false;
1776bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
1777 const wxString& aTempRestorePath,
bool aKeepAllFiles,
1778 std::set<wxString>& aBackedUpFiles )
1780 wxDir currentDir( aProjectPath );
1781 if( !currentDir.IsOpened() )
1785 bool cont = currentDir.GetFirst( &filename );
1791 if( !isRestoreProtectedEntry( filename ) )
1794 bool shouldBackup = !aKeepAllFiles;
1799 wxFileName testPath( aTempRestorePath, filename );
1800 shouldBackup = testPath.Exists();
1805 wxFileName source( aProjectPath, filename );
1806 wxFileName dest( aBackupPath, filename );
1809 if( !wxDirExists( aBackupPath ) )
1812 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
1814 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
1818 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
1819 source.GetFullPath(), dest.GetFullPath() );
1821 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1824 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
1825 source.GetFullPath() );
1829 aBackedUpFiles.insert( filename );
1832 cont = currentDir.GetNext( &filename );
1842bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
1843 std::set<wxString>& aRestoredFiles )
1845 wxDir tempDir( aTempRestorePath );
1846 if( !tempDir.IsOpened() )
1850 bool cont = tempDir.GetFirst( &filename );
1854 wxFileName source( aTempRestorePath, filename );
1855 wxFileName dest( aProjectPath, filename );
1858 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
1859 source.GetFullPath(), dest.GetFullPath() );
1861 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1864 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
1865 source.GetFullPath() );
1869 aRestoredFiles.insert( filename );
1870 cont = tempDir.GetNext( &filename );
1880void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
1881 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
1882 const std::set<wxString>& aRestoredFiles )
1884 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
1888 for(
const wxString& filename : aRestoredFiles )
1890 wxFileName toRemove( aProjectPath, filename );
1891 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
1892 toRemove.GetFullPath() );
1894 if( toRemove.DirExists() )
1896 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1898 else if( toRemove.FileExists() )
1900 wxRemoveFile( toRemove.GetFullPath() );
1905 if( wxDirExists( aBackupPath ) )
1907 for(
const wxString& filename : aBackedUpFiles )
1909 wxFileName source( aBackupPath, filename );
1910 wxFileName dest( aProjectPath, filename );
1912 if( source.Exists() )
1914 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1915 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
1916 dest.GetFullPath() );
1922 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
1923 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
1930bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
1931 const wxString& aHash )
1933 git_time_t t = git_commit_time( aCommit );
1934 wxDateTime dt( (time_t) t );
1935 git_signature* sig =
nullptr;
1937 git_commit* parent =
nullptr;
1940 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
1941 git_commit_lookup( &parent, aRepo, &parent_id );
1944 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1947 const git_commit* constParent = parent;
1948 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
1949 msg.mb_str().data(), aTree, parent ? 1 : 0,
1950 parent ? &constParent :
nullptr );
1953 git_commit_free( parent );
1954 git_signature_free( sig );
1966 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
1969 std::vector<wxString> lockedFiles;
1970 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
1973 for(
const auto& f : lockedFiles )
1974 lockList += wxS(
"\n - ") + f;
1977 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
1983 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
1985 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
1996 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
2007 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
2009 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
2013 git_commit* commit =
nullptr;
2014 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2016 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
2020 git_tree* tree =
nullptr;
2021 git_commit_tree( &tree, commit );
2024 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
2026 std::vector<wxString> backupFiles;
2029 if( !backupFiles.empty() )
2033 backupFiles, wxS(
"Pre-restore backup" ) );
2038 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
2039 git_tree_free( tree );
2040 git_commit_free( commit );
2046 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Current state already matches HEAD; "
2047 "continuing without a new backup commit" ) );
2052 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
2054 if( wxDirExists( tempRestorePath ) )
2055 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2057 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
2060 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
2062 git_tree_free( tree );
2063 git_commit_free( commit );
2067 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
2070 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
2072 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
2073 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2074 git_tree_free( tree );
2075 git_commit_free( commit );
2080 std::set<wxString> restoredFiles;
2081 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
2083 std::vector<wxString> filesToDelete;
2084 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
2086 bool keepAllFiles =
true;
2087 if( !confirmFileDeletion( aParent, filesToDelete, keepAllFiles ) )
2090 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2091 git_tree_free( tree );
2092 git_commit_free( commit );
2097 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
2099 wxString backupPath = aProjectPath + wxS(
"_restore_backup");
2102 if( wxDirExists( backupPath ) )
2104 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Removing old backup %s" ),
2106 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
2110 std::set<wxString> backedUpFiles;
2111 std::set<wxString> restoredFilesSet;
2114 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
2117 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2119 git_tree_free( tree );
2120 git_commit_free( commit );
2125 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
2127 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2129 git_tree_free( tree );
2130 git_commit_free( commit );
2135 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Restore successful, cleaning up" ) );
2136 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2137 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
2140 recordRestoreInHistory( repo, commit, tree, aHash );
2142 git_tree_free( tree );
2143 git_commit_free( commit );
2145 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
2154 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots =
LoadSnapshots( aProjectPath );
2156 if( snapshots.empty() )
2165 if( !selectedHash.IsEmpty() )
2172 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2175 git_repository* repo =
nullptr;
2177 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2180 git_revwalk* walk =
nullptr;
2181 if( git_revwalk_new( &walk, repo ) != 0 )
2183 git_repository_free( repo );
2187 git_revwalk_sorting( walk, GIT_SORT_TIME );
2188 git_revwalk_push_head( walk );
2192 while( git_revwalk_next( &oid, walk ) == 0 )
2194 git_commit* commit =
nullptr;
2196 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2200 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2201 info.date = wxDateTime(
static_cast<time_t
>( git_commit_time( commit ) ) );
2202 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2204 wxString firstLine =
info.message.BeforeFirst(
'\n' );
2206 long parsedCount = 0;
2208 firstLine.BeforeFirst(
':', &remainder );
2209 remainder.Trim(
true ).Trim(
false );
2211 if( remainder.EndsWith( wxS(
"files changed" ) ) )
2213 wxString countText = remainder.BeforeFirst(
' ' );
2215 if( countText.ToLong( &parsedCount ) )
2216 info.filesChanged =
static_cast<int>( parsedCount );
2219 info.summary = firstLine.BeforeFirst(
':' );
2222 info.message.BeforeFirst(
'\n', &rest );
2223 wxArrayString lines = wxSplit( rest,
'\n',
'\0' );
2225 for(
const wxString& line : lines )
2227 if( !line.IsEmpty() )
2228 info.changedFiles.Add( line );
2231 snapshots.push_back( std::move(
info ) );
2232 git_commit_free( commit );
2235 git_revwalk_free( walk );
2236 git_repository_free( repo );
wxString GetSelectedHash() const
Hybrid locking mechanism for local history git repositories.
git_repository * GetRepository()
Get the git repository handle (only valid if IsLocked() returns true).
void ReleaseRepository()
Release git repository and index handles early, but keep the file lock.
wxString GetLockError() const
Get error message describing why lock could not be acquired.
git_index * GetIndex()
Get the git index handle (only valid if IsLocked() returns true).
bool IsLocked() const
Check if locks were successfully acquired.
std::vector< LOCAL_HISTORY_SNAPSHOT_INFO > LoadSnapshots(const wxString &aProjectPath)
bool EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes, PROGRESS_REPORTER *aReporter=nullptr)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
bool TagSave(const wxString &aProjectPath, const wxString &aFileType)
Tag a manual save in the local history repository.
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
std::map< const void *, std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> > m_savers
bool CommitDuplicateOfLastSave(const wxString &aProjectPath, const wxString &aFileType, const wxString &aMessage)
Create a new commit duplicating the tree pointed to by Last_Save_<fileType> and move the Last_Save_<f...
bool commitInBackground(const wxString &aProjectPath, const wxString &aTitle, const std::vector< HISTORY_FILE_DATA > &aFileData)
Execute file writes and git commit on a background thread.
void WaitForPendingSave()
Block until any pending background save completes.
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> &aSaver)
Register a saver callback invoked during autosave history commits.
bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
void ClearAllSavers()
Clear all registered savers.
bool CommitSnapshot(const std::vector< wxString > &aFiles, const wxString &aTitle)
Commit the given files to the local history repository.
std::atomic< bool > m_saveInProgress
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...
std::future< bool > m_pendingFuture
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
A progress reporter interface for use in multi-threaded environments.
virtual void Report(const wxString &aMessage)=0
Display aMessage in the progress bar dialog.
virtual void AdvancePhase()=0
Use the next available virtual zone of the dialog progress bar.
virtual void SetCurrentProgress(double aProgress)=0
Set the progress value to aProgress (0..1).
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 bool compactRepository(git_repository *aRepo, PROGRESS_REPORTER *aReporter=nullptr)
static size_t dirSizeRecursive(const wxString &path)
static bool copyTreeObjects(git_repository *aSrcRepo, git_odb *aSrcOdb, git_odb *aDstOdb, const git_oid *aTreeOid, std::set< git_oid, bool(*)(const git_oid &, const git_oid &)> &aCopied)
static SNAPSHOT_COMMIT_RESULT commitSnapshotWithLock(git_repository *repo, git_index *index, const wxString &aHistoryPath, const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
static void collectProjectFiles(const wxString &aProjectPath, std::vector< wxString > &aFiles)
PGM_BASE & Pgm()
The global program "get" accessor.
#define PROJECT_BACKUPS_DIR_SUFFIX
Project settings path will be <projectname> + this.
Data produced by a registered saver on the UI thread, consumed by the background commit thread.
wxString result
Test unit parsing edge cases and error handling.
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
wxLogTrace helper definitions.
Definition of file extensions used in Kicad.