38#include <wx/filename.h>
42#include <wx/datetime.h>
57 wxFileName p( aProjectPath, wxEmptyString );
58 p.AppendDir( wxS(
".history" ) );
73 wxFileName fn( aFile );
75 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
83 const void* aSaverObject,
84 const std::function<
void(
const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
88 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping"), aSaverObject );
93 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
101 auto it =
m_savers.find( aSaverObject );
106 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
115 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers") );
121 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
123 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
127 Init( aProjectPath );
129 wxLogTrace(
traceAutoSave, wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu"),
130 aProjectPath, aTitle,
m_savers.size() );
134 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
141 wxLogTrace(
traceAutoSave, wxS(
"[history] previous save still in progress; skipping cycle") );
146 std::vector<HISTORY_FILE_DATA> fileData;
148 for(
const auto& [saverObject, saver] :
m_savers )
150 size_t before = fileData.size();
151 saver( aProjectPath, fileData );
152 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p produced %zu entries (total=%zu)"),
153 saverObject, fileData.size() - before, fileData.size() );
157 wxString projectDir = aProjectPath;
158 if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
159 projectDir += wxFileName::GetPathSeparator();
161 auto it = std::remove_if( fileData.begin(), fileData.end(),
164 if( !entry.path.StartsWith( projectDir ) )
166 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out entry outside project: %s"), entry.path );
171 fileData.erase( it, fileData.end() );
173 if( fileData.empty() )
175 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no entries; skipping") );
180 m_saveInProgress.store(
true, std::memory_order_release );
183 [
this, projectPath = aProjectPath, title = aTitle,
184 data = std::move( fileData )]()
mutable ->
bool
186 bool result = commitInBackground( projectPath, title, data );
187 m_saveInProgress.store(
false, std::memory_order_release );
196 const std::vector<HISTORY_FILE_DATA>& aFileData )
198 wxLogTrace(
traceAutoSave, wxS(
"[history] background: writing %zu entries for '%s'"),
199 aFileData.size(), aProjectPath );
206 if( !entry.content.empty() )
208 std::string buf = entry.content;
213 wxFFile fp( entry.path, wxS(
"wb" ) );
217 fp.Write( buf.data(), buf.size() );
219 wxLogTrace(
traceAutoSave, wxS(
"[history] background: wrote %zu bytes to '%s'"),
220 buf.size(), entry.path );
224 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to open '%s' for writing"),
228 else if( !entry.sourcePath.IsEmpty() )
230 wxCopyFile( entry.sourcePath, entry.path,
true );
231 wxLogTrace(
traceAutoSave, wxS(
"[history] background: copied '%s' -> '%s'"),
232 entry.sourcePath, entry.path );
248 git_repository_set_workdir( repo, hist.mb_str().data(),
false );
253 wxFileName src( entry.path );
255 if( !src.FileExists() )
258 if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
260 std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
261 git_index_add_bypath(
index, relHist.c_str() );
267 git_commit* head_commit =
nullptr;
268 git_tree* head_tree =
nullptr;
270 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
271 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
272 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
274 git_tree* rawIndexTree =
nullptr;
275 git_oid index_tree_oid;
277 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
279 if( head_tree ) git_tree_free( head_tree );
280 if( head_commit ) git_commit_free( head_commit );
281 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to write index tree" ) );
285 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
286 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
288 bool hasChanges =
true;
292 git_diff* diff =
nullptr;
294 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
296 hasChanges = git_diff_num_deltas( diff ) > 0;
297 wxLogTrace(
traceAutoSave, wxS(
"[history] background: diff deltas=%u"), (
unsigned) git_diff_num_deltas( diff ) );
298 git_diff_free( diff );
302 if( head_tree ) git_tree_free( head_tree );
303 if( head_commit ) git_commit_free( head_commit );
307 wxLogTrace(
traceAutoSave, wxS(
"[history] background: no changes detected; no commit") );
311 git_signature* rawSig =
nullptr;
313 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
315 git_commit* parent =
nullptr;
319 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
321 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
325 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
327 const git_commit* constParent = parent;
329 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
330 msg.mb_str().data(), indexTree.get(), parents,
331 parents ? &constParent :
nullptr );
334 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit created %s (%s entries=%zu)"),
335 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
337 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit failed rc=%d"), rc );
339 if( parent ) git_commit_free( parent );
341 git_index_write(
index );
350 wxLogTrace(
traceAutoSave, wxS(
"[history] waiting for pending background save") );
366 if( aProjectPath.IsEmpty() )
369 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
374 if( !wxDirExists( hist ) )
376 if( wxIsWritable( aProjectPath ) )
378 if( !wxMkdir( hist ) )
385 git_repository* rawRepo =
nullptr;
386 bool isNewRepo =
false;
388 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
390 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
395 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
396 if( !ignoreFile.FileExists() )
398 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
401 f.Write( wxS(
"fp-info-cache\n*-backups\nREADME.txt\n" ) );
406 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
408 if( !readmeFile.FileExists() )
410 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
414 f.Write( wxS(
"KiCad Local History Directory\n"
415 "=============================\n\n"
416 "This directory contains automatic snapshots of your project files.\n"
417 "KiCad periodically saves copies of your work here, allowing you to\n"
418 "recover from accidental changes or data loss.\n\n"
419 "You can browse and restore previous versions through KiCad's\n"
420 "File > Local History menu.\n\n"
421 "To disable this feature:\n"
422 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
423 "This directory can be safely deleted if you no longer need the\n"
424 "history, but doing so will permanently remove all saved snapshots.\n" ) );
430 git_repository_free( rawRepo );
436 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: New repository created, collecting existing files" ) );
440 std::function<void(
const wxString& )> collect = [&](
const wxString&
path )
448 bool cont = d.GetFirst( &
name );
453 if(
name.StartsWith( wxS(
"." ) ) ||
name.EndsWith( wxS(
"-backups" ) ) )
455 cont = d.GetNext( &
name );
460 wxString fullPath = fn.GetFullPath();
462 if( wxFileName::DirExists( fullPath ) )
466 else if( fn.FileExists() )
469 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
470 files.Add( fn.GetFullPath() );
473 cont = d.GetNext( &
name );
477 collect( aProjectPath );
479 if( files.GetCount() > 0 )
481 std::vector<wxString> vec;
482 vec.reserve( files.GetCount() );
484 for(
unsigned i = 0; i < files.GetCount(); ++i )
485 vec.push_back( files[i] );
487 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: Creating initial snapshot with %zu files" ), vec.size() );
492 TagSave( aProjectPath, wxS(
"project" ) );
496 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: No files found to add to initial snapshot" ) );
514 const wxString& aHistoryPath,
const wxString& aProjectPath,
515 const std::vector<wxString>& aFiles,
const wxString& aTitle )
517 std::vector<std::string> filesArrStr;
519 for(
const wxString& file : aFiles )
521 wxFileName src( file );
524 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
525 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
527 relPath = src.GetFullName();
529 relPath.Replace(
"\\",
"/" );
530 std::string relPathStr = relPath.ToStdString();
532 unsigned int status = 0;
533 int rc = git_status_file( &status, repo, relPathStr.data() );
535 if( rc == 0 && status != 0 )
537 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
538 filesArrStr.emplace_back( relPathStr );
542 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
543 filesArrStr.emplace_back( relPathStr );
547 std::vector<char*> cStrings( filesArrStr.size() );
549 for(
size_t i = 0; i < filesArrStr.size(); i++ )
550 cStrings[i] = filesArrStr[i].data();
552 git_strarray filesArrGit;
553 filesArrGit.count = filesArrStr.size();
554 filesArrGit.strings = cStrings.data();
556 if( filesArrStr.size() == 0 )
562 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
564 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
570 if( git_index_write_tree( &tree_id,
index ) != 0 )
573 git_tree* rawTree =
nullptr;
574 git_tree_lookup( &rawTree, repo, &tree_id );
575 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
577 git_signature* rawSig =
nullptr;
579 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
580 &git_signature_free );
582 git_commit* rawParent =
nullptr;
586 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
588 git_commit_lookup( &rawParent, repo, &parent_id );
592 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
595 git_tree* rawParentTree =
nullptr;
598 git_commit_tree( &rawParentTree, parent.get() );
600 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
602 git_diff* rawDiff =
nullptr;
603 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
604 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
606 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
608 if( numChangedFiles == 0 )
610 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
616 if( !aTitle.IsEmpty() )
617 msg << aTitle << wxS(
": " );
619 msg << numChangedFiles << wxS(
" files changed" );
621 for(
size_t i = 0; i < numChangedFiles; ++i )
623 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
624 git_patch* rawPatch =
nullptr;
625 git_patch_from_diff( &rawPatch, diff.get(), i );
626 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
628 size_t context = 0, adds = 0, dels = 0;
629 git_patch_line_stats( &context, &adds, &dels, patch.get() );
630 size_t updated = std::min( adds, dels );
633 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
634 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
638 git_commit* parentPtr = parent.get();
639 const git_commit* constParentPtr = parentPtr;
640 if( git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr, msg.mb_str().data(), tree.get(),
641 parents, parentPtr ? &constParentPtr :
nullptr )
647 git_index_write(
index );
654 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled )
657 wxString proj = wxFileName( aFiles[0] ).GetPath();
668 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitSnapshot failed to acquire lock: %s"),
683 wxDir dir( aProjectPath );
685 if( !dir.IsOpened() )
689 std::function<void(
const wxString&)> collect = [&](
const wxString&
path )
697 bool cont = d.GetFirst( &
name );
701 if(
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) )
703 cont = d.GetNext( &
name );
708 wxString fullPath = fn.GetFullPath();
710 if( wxFileName::DirExists( fullPath ) )
714 else if( fn.FileExists() )
717 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
718 aFiles.push_back( fn.GetFullPath() );
721 cont = d.GetNext( &
name );
725 collect( aProjectPath );
731 std::vector<wxString> files;
747 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
754 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
764 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
769 git_reference* ref =
nullptr;
772 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
773 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
776 git_object* head_obj =
nullptr;
777 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
778 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
779 git_object_free( head_obj );
782 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
783 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
785 git_reference_delete( ref );
786 git_reference_free( ref );
789 git_oid last_tag_oid;
790 git_object* head_obj2 =
nullptr;
791 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
792 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
793 git_object_free( head_obj2 );
801 git_repository* repo =
nullptr;
803 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
807 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
809 git_repository_free( repo );
813 git_commit* head_commit =
nullptr;
814 git_commit_lookup( &head_commit, repo, &head_oid );
815 git_time_t head_time = git_commit_time( head_commit );
818 git_tag_list_match( &tags,
"Last_Save_*", repo );
819 git_time_t save_time = 0;
821 for(
size_t i = 0; i < tags.count; ++i )
823 git_reference* ref =
nullptr;
824 if( git_reference_lookup( &ref, repo,
825 ( wxS(
"refs/tags/" ) +
826 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
828 const git_oid* oid = git_reference_target( ref );
829 git_commit* c =
nullptr;
830 if( git_commit_lookup( &c, repo, oid ) == 0 )
832 git_time_t t = git_commit_time( c );
835 git_commit_free( c );
837 git_reference_free( ref );
841 git_strarray_free( &tags );
842 git_commit_free( head_commit );
843 git_repository_free( repo );
850 return head_time > save_time;
854 const wxString& aMessage )
856 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
863 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
872 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
873 git_reference* lastRef =
nullptr;
874 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
876 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
878 const git_oid* lastOid = git_reference_target( lastRef );
879 git_commit* lastCommit =
nullptr;
880 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
882 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
884 git_tree* lastTree =
nullptr;
885 git_commit_tree( &lastTree, lastCommit );
886 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
890 git_commit* headCommit =
nullptr;
892 const git_commit* parentArray[1];
893 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
894 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
896 parentArray[0] = headCommit;
900 git_signature* sigRaw =
nullptr;
902 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
904 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
905 git_oid newCommitOid;
906 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
907 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
908 if( headCommit ) git_commit_free( headCommit );
913 git_reference* existing =
nullptr;
914 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
916 git_reference_delete( existing );
917 git_reference_free( existing );
919 git_object* newCommitObj =
nullptr;
920 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
922 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
923 git_object_free( newCommitObj );
932 if( !dir.IsOpened() )
935 bool cont = dir.GetFirst( &
name );
939 wxString fullPath = fn.GetFullPath();
941 if( wxFileName::DirExists( fullPath ) )
943 else if( fn.FileExists() )
944 total += (size_t) fn.GetSize().GetValue();
945 cont = dir.GetNext( &
name );
951static bool copyTreeObjects( git_repository* aSrcRepo, git_odb* aSrcOdb, git_odb* aDstOdb,
const git_oid* aTreeOid,
952 std::set<git_oid,
bool ( * )(
const git_oid&,
const git_oid& )>& aCopied )
954 if( aCopied.count( *aTreeOid ) )
957 git_odb_object* obj =
nullptr;
959 if( git_odb_read( &obj, aSrcOdb, aTreeOid ) != 0 )
963 int err = git_odb_write( &written, aDstOdb, git_odb_object_data( obj ), git_odb_object_size( obj ),
964 git_odb_object_type( obj ) );
965 git_odb_object_free( obj );
970 aCopied.insert( *aTreeOid );
972 git_tree* tree =
nullptr;
974 if( git_tree_lookup( &tree, aSrcRepo, aTreeOid ) != 0 )
977 size_t cnt = git_tree_entrycount( tree );
979 for(
size_t i = 0; i < cnt; ++i )
981 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
982 const git_oid* entryId = git_tree_entry_id( entry );
984 if( aCopied.count( *entryId ) )
987 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
991 git_tree_free( tree );
995 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
997 git_odb_object* blobObj =
nullptr;
999 if( git_odb_read( &blobObj, aSrcOdb, entryId ) == 0 )
1001 git_oid blobWritten;
1003 if( git_odb_write( &blobWritten, aDstOdb, git_odb_object_data( blobObj ),
1004 git_odb_object_size( blobObj ), git_odb_object_type( blobObj ) )
1007 git_odb_object_free( blobObj );
1008 git_tree_free( tree );
1012 git_odb_object_free( blobObj );
1013 aCopied.insert( *entryId );
1018 git_tree_free( tree );
1027 git_packbuilder* pb =
nullptr;
1029 if( git_packbuilder_new( &pb, aRepo ) != 0 )
1032 git_revwalk* walk =
nullptr;
1034 if( git_revwalk_new( &walk, aRepo ) != 0 )
1036 git_packbuilder_free( pb );
1040 git_revwalk_push_head( walk );
1043 while( git_revwalk_next( &oid, walk ) == 0 )
1045 if( git_packbuilder_insert_commit( pb, &oid ) != 0 )
1047 git_revwalk_free( walk );
1048 git_packbuilder_free( pb );
1053 git_revwalk_free( walk );
1057 git_packbuilder_set_callbacks(
1059 [](
int aStage, uint32_t aCurrent, uint32_t aTotal,
void* aPayload )
1066 reporter->KeepRefreshing();
1072 if( git_packbuilder_write( pb,
nullptr, 0,
nullptr,
nullptr ) != 0 )
1074 git_packbuilder_free( pb );
1078 git_packbuilder_free( pb );
1080 wxString objPath = wxString::FromUTF8( git_repository_path( aRepo ) ) + wxS(
"objects" );
1081 wxDir objDir( objPath );
1083 if( objDir.IsOpened() )
1085 wxArrayString toRemove;
1087 bool cont = objDir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS );
1091 if(
name.length() == 2 )
1092 toRemove.Add( objPath + wxFileName::GetPathSeparator() +
name );
1094 cont = objDir.GetNext( &
name );
1097 for(
const wxString& dir : toRemove )
1098 wxFileName::Rmdir( dir, wxPATH_RMDIR_RECURSIVE );
1107 if( aMaxBytes == 0 )
1112 if( !wxDirExists( hist ) )
1117 if( current <= aMaxBytes )
1124 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
1134 aReporter->
Report(
_(
"Compacting local history..." ) );
1141 if( current <= aMaxBytes )
1145 git_revwalk* walk =
nullptr;
1146 git_revwalk_new( &walk, repo );
1147 git_revwalk_sorting( walk, GIT_SORT_TIME );
1148 git_revwalk_push_head( walk );
1149 std::vector<git_oid> commits;
1152 while( git_revwalk_next( &oid, walk ) == 0 )
1153 commits.push_back( oid );
1155 git_revwalk_free( walk );
1157 if( commits.empty() )
1161 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
1162 [](
const git_oid& a,
const git_oid& b )
1164 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1167 size_t keptBytes = 0;
1168 std::vector<git_oid> keep;
1170 git_odb* odb =
nullptr;
1171 git_repository_odb( &odb, repo );
1173 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
1176 size_t cnt = git_tree_entrycount( tree );
1178 for(
size_t i = 0; i < cnt; ++i )
1180 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1182 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1184 const git_oid* bid = git_tree_entry_id( entry );
1186 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1189 git_object_t type = GIT_OBJECT_ANY;
1191 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1194 seenBlobs.insert( *bid );
1197 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1199 git_tree* sub =
nullptr;
1201 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1203 added += accountTree( sub );
1204 git_tree_free( sub );
1212 for(
const git_oid& cOid : commits )
1214 git_commit* c =
nullptr;
1216 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1219 git_tree* tree =
nullptr;
1220 git_commit_tree( &tree, c );
1221 size_t add = accountTree( tree );
1222 git_tree_free( tree );
1223 git_commit_free( c );
1225 if( keep.empty() || keptBytes + add <= aMaxBytes )
1227 keep.push_back( cOid );
1235 keep.push_back( commits.front() );
1239 std::vector<std::pair<wxString, git_oid>> tagTargets;
1240 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
1241 [](
const git_oid& a,
const git_oid& b )
1243 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1245 git_strarray tagList;
1247 if( git_tag_list( &tagList, repo ) == 0 )
1249 for(
size_t i = 0; i < tagList.count; ++i )
1251 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1252 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1254 git_reference* tref =
nullptr;
1256 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1258 const git_oid* toid = git_reference_target( tref );
1262 tagTargets.emplace_back(
name, *toid );
1263 taggedCommits.insert( *toid );
1267 for(
const auto& k : keep )
1269 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1279 keep.push_back( *toid );
1280 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1285 git_reference_free( tref );
1289 git_strarray_free( &tagList );
1293 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1294 wxString trimPath = trimFn.GetPath();
1296 if( wxDirExists( trimPath ) )
1297 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1299 wxMkdir( trimPath );
1300 git_repository* newRepo =
nullptr;
1302 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1304 git_odb_free( odb );
1308 git_odb* dstOdb =
nullptr;
1310 if( git_repository_odb( &dstOdb, newRepo ) != 0 )
1312 git_repository_free( newRepo );
1313 git_odb_free( odb );
1317 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> copiedObjects(
1318 [](
const git_oid& a,
const git_oid& b )
1320 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1324 std::reverse( keep.begin(), keep.end() );
1325 git_commit* parent =
nullptr;
1326 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1327 std::vector<MAP_ENTRY> commitMap;
1331 aReporter->
AdvancePhase(
_(
"Trimming local history..." ) );
1335 for(
size_t idx = 0; idx < keep.size(); ++idx )
1340 const git_oid& co = keep[idx];
1341 git_commit* orig =
nullptr;
1343 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1346 git_tree* tree =
nullptr;
1347 git_commit_tree( &tree, orig );
1349 copyTreeObjects( repo, odb, dstOdb, git_tree_id( tree ), copiedObjects );
1351 git_tree* newTree =
nullptr;
1352 git_tree_lookup( &newTree, newRepo, git_tree_id( tree ) );
1354 git_tree_free( tree );
1357 const git_signature* origAuthor = git_commit_author( orig );
1358 const git_signature* origCommitter = git_commit_committer( orig );
1359 git_signature* sigAuthor =
nullptr;
1360 git_signature* sigCommitter =
nullptr;
1362 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1363 origAuthor->when.time, origAuthor->when.offset );
1364 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1365 origCommitter->when.time, origCommitter->when.offset );
1367 const git_commit* parents[1];
1368 int parentCount = 0;
1372 parents[0] = parent;
1376 git_oid newCommitOid;
1377 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1378 newTree, parentCount, parentCount ? parents :
nullptr );
1381 git_commit_free( parent );
1383 git_commit_lookup( &parent, newRepo, &newCommitOid );
1385 commitMap.emplace_back( co, newCommitOid );
1387 git_signature_free( sigAuthor );
1388 git_signature_free( sigCommitter );
1389 git_tree_free( newTree );
1390 git_commit_free( orig );
1394 git_commit_free( parent );
1397 for(
const auto& tt : tagTargets )
1400 const git_oid* newOid =
nullptr;
1402 for(
const auto& m : commitMap )
1404 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1414 git_object* obj =
nullptr;
1416 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1418 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1419 git_object_free( obj );
1424 aReporter->
AdvancePhase(
_(
"Compacting trimmed history..." ) );
1431 git_odb_free( dstOdb );
1432 git_odb_free( odb );
1433 git_repository_free( newRepo );
1438 wxString backupOld = hist + wxS(
"_old");
1439 wxRenameFile( hist, backupOld );
1440 wxRenameFile( trimPath, hist );
1441 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1448 git_repository* repo =
nullptr;
1450 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1451 return wxEmptyString;
1454 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1456 git_repository_free( repo );
1457 return wxEmptyString;
1460 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1461 git_repository_free( repo );
1473bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1475 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
1477 wxDir dir( dirPath );
1478 if( !dir.IsOpened() )
1482 bool cont = dir.GetFirst( &filename );
1486 wxFileName fullPath( dirPath, filename );
1489 if( filename == wxS(
".history") || filename == wxS(
".git") )
1491 cont = dir.GetNext( &filename );
1495 if( fullPath.DirExists() )
1497 findLocks( fullPath.GetFullPath() );
1499 else if( fullPath.FileExists()
1506 baseName = baseName.BeforeLast(
'.' );
1507 wxFileName originalFile( dirPath, baseName );
1510 LOCKFILE testLock( originalFile.GetFullPath() );
1511 if( testLock.Valid() && !testLock.IsLockedByMe() )
1513 aLockedFiles.push_back( fullPath.GetFullPath() );
1517 cont = dir.GetNext( &filename );
1521 findLocks( aProjectPath );
1522 return aLockedFiles.empty();
1529bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
1531 bool extractSuccess =
true;
1533 std::function<void( git_tree*,
const wxString& )> extractTree =
1534 [&]( git_tree* t,
const wxString& prefix )
1536 if( !extractSuccess )
1539 size_t cnt = git_tree_entrycount( t );
1540 for(
size_t i = 0; i < cnt; ++i )
1542 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1543 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1544 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
1546 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1548 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
1550 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1553 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
1554 dirPath.GetPath() );
1555 extractSuccess =
false;
1559 git_tree* sub =
nullptr;
1560 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
1562 extractTree( sub, fullPath );
1563 git_tree_free( sub );
1566 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1568 git_blob* blob =
nullptr;
1569 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
1571 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
1573 wxFileName dstDir( dst );
1574 dstDir.SetFullName( wxEmptyString );
1575 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1577 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
1580 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1586 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
1587 dst.GetFullPath() );
1588 extractSuccess =
false;
1589 git_blob_free( blob );
1593 git_blob_free( blob );
1599 extractTree( aTree, wxEmptyString );
1600 return extractSuccess;
1607void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
1608 std::set<wxString>& aFiles )
1610 wxDir dir( aSearchPath );
1611 if( !dir.IsOpened() )
1615 bool cont = dir.GetFirst( &filename );
1619 wxFileName fullPath( aSearchPath, filename );
1620 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
1622 if( fullPath.IsDir() && fullPath.DirExists() )
1624 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
1626 else if( fullPath.FileExists() )
1628 aFiles.insert( relativePath );
1631 cont = dir.GetNext( &filename );
1639bool shouldExcludeFromBackup(
const wxString& aFilename )
1642 return aFilename == wxS(
"fp-info-cache" );
1649void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
1650 std::vector<wxString>& aFilesToDelete )
1652 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
1653 [&](
const wxString& dirPath,
const wxString& relativeBase )
1655 wxDir dir( dirPath );
1656 if( !dir.IsOpened() )
1660 bool cont = dir.GetFirst( &filename );
1665 if( filename == wxS(
".history") || filename == wxS(
".git") ||
1666 filename == wxS(
"_restore_backup") || filename == wxS(
"_restore_temp") )
1668 cont = dir.GetNext( &filename );
1672 wxFileName fullPath( dirPath, filename );
1673 wxString relativePath = relativeBase.IsEmpty() ? filename :
1674 relativeBase + wxS(
"/") + filename;
1676 if( fullPath.IsDir() && fullPath.DirExists() )
1678 scanDirectory( fullPath.GetFullPath(), relativePath );
1680 else if( fullPath.FileExists() )
1683 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
1686 if( !shouldExcludeFromBackup( filename ) )
1687 aFilesToDelete.push_back( relativePath );
1691 cont = dir.GetNext( &filename );
1695 scanDirectory( aProjectPath, wxEmptyString );
1703bool confirmFileDeletion( wxWindow* aParent,
const std::vector<wxString>& aFilesToDelete,
1704 bool& aKeepAllFiles )
1706 if( aFilesToDelete.empty() || !aParent )
1708 aKeepAllFiles =
false;
1712 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
1715 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
1716 for(
size_t i = 0; i < displayCount; ++i )
1718 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
1721 if( aFilesToDelete.size() > displayCount )
1723 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
1724 aFilesToDelete.size() - displayCount );
1728 wxYES_NO | wxCANCEL | wxICON_QUESTION );
1729 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
1730 dlg.SetExtendedMessage(
1731 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
1732 "files in the project directory. Choosing 'Proceed' will delete files that are not "
1733 "present in the restored commit." ) );
1735 int choice = dlg.ShowModal();
1737 if( choice == wxID_CANCEL )
1739 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
1742 else if( choice == wxID_NO )
1744 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
1745 aKeepAllFiles =
true;
1749 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
1750 aKeepAllFiles =
false;
1760bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
1761 const wxString& aTempRestorePath,
bool aKeepAllFiles,
1762 std::set<wxString>& aBackedUpFiles )
1764 wxDir currentDir( aProjectPath );
1765 if( !currentDir.IsOpened() )
1769 bool cont = currentDir.GetFirst( &filename );
1773 if( filename != wxS(
".history" ) && filename != wxS(
".git" ) &&
1774 filename != wxS(
"_restore_backup" ) && filename != wxS(
"_restore_temp" ) )
1777 bool shouldBackup = !aKeepAllFiles;
1782 wxFileName testPath( aTempRestorePath, filename );
1783 shouldBackup = testPath.Exists();
1788 wxFileName source( aProjectPath, filename );
1789 wxFileName dest( aBackupPath, filename );
1792 if( !wxDirExists( aBackupPath ) )
1795 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
1797 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
1801 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
1802 source.GetFullPath(), dest.GetFullPath() );
1804 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1807 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
1808 source.GetFullPath() );
1812 aBackedUpFiles.insert( filename );
1815 cont = currentDir.GetNext( &filename );
1825bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
1826 std::set<wxString>& aRestoredFiles )
1828 wxDir tempDir( aTempRestorePath );
1829 if( !tempDir.IsOpened() )
1833 bool cont = tempDir.GetFirst( &filename );
1837 wxFileName source( aTempRestorePath, filename );
1838 wxFileName dest( aProjectPath, filename );
1841 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
1842 source.GetFullPath(), dest.GetFullPath() );
1844 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1847 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
1848 source.GetFullPath() );
1852 aRestoredFiles.insert( filename );
1853 cont = tempDir.GetNext( &filename );
1863void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
1864 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
1865 const std::set<wxString>& aRestoredFiles )
1867 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
1871 for(
const wxString& filename : aRestoredFiles )
1873 wxFileName toRemove( aProjectPath, filename );
1874 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
1875 toRemove.GetFullPath() );
1877 if( toRemove.DirExists() )
1879 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1881 else if( toRemove.FileExists() )
1883 wxRemoveFile( toRemove.GetFullPath() );
1888 if( wxDirExists( aBackupPath ) )
1890 for(
const wxString& filename : aBackedUpFiles )
1892 wxFileName source( aBackupPath, filename );
1893 wxFileName dest( aProjectPath, filename );
1895 if( source.Exists() )
1897 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1898 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
1899 dest.GetFullPath() );
1905 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
1906 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
1913bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
1914 const wxString& aHash )
1916 git_time_t t = git_commit_time( aCommit );
1917 wxDateTime dt( (time_t) t );
1918 git_signature* sig =
nullptr;
1920 git_commit* parent =
nullptr;
1923 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
1924 git_commit_lookup( &parent, aRepo, &parent_id );
1927 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1930 const git_commit* constParent = parent;
1931 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
1932 msg.mb_str().data(), aTree, parent ? 1 : 0,
1933 parent ? &constParent :
nullptr );
1936 git_commit_free( parent );
1937 git_signature_free( sig );
1949 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
1952 std::vector<wxString> lockedFiles;
1953 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
1956 for(
const auto& f : lockedFiles )
1957 lockList += wxS(
"\n - ") + f;
1960 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
1966 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
1968 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
1979 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
1990 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
1992 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
1996 git_commit* commit =
nullptr;
1997 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
1999 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
2003 git_tree* tree =
nullptr;
2004 git_commit_tree( &tree, commit );
2007 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
2009 std::vector<wxString> backupFiles;
2012 if( !backupFiles.empty() )
2016 backupFiles, wxS(
"Pre-restore backup" ) );
2021 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
2022 git_tree_free( tree );
2023 git_commit_free( commit );
2029 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Current state already matches HEAD; "
2030 "continuing without a new backup commit" ) );
2035 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
2037 if( wxDirExists( tempRestorePath ) )
2038 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2040 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
2043 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
2045 git_tree_free( tree );
2046 git_commit_free( commit );
2050 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
2053 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
2055 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
2056 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2057 git_tree_free( tree );
2058 git_commit_free( commit );
2063 std::set<wxString> restoredFiles;
2064 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
2066 std::vector<wxString> filesToDelete;
2067 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
2069 bool keepAllFiles =
true;
2070 if( !confirmFileDeletion( aParent, filesToDelete, keepAllFiles ) )
2073 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2074 git_tree_free( tree );
2075 git_commit_free( commit );
2080 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
2082 wxString backupPath = aProjectPath + wxS(
"_restore_backup");
2085 if( wxDirExists( backupPath ) )
2087 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Removing old backup %s" ),
2089 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
2093 std::set<wxString> backedUpFiles;
2094 std::set<wxString> restoredFilesSet;
2097 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
2100 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2102 git_tree_free( tree );
2103 git_commit_free( commit );
2108 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
2110 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2112 git_tree_free( tree );
2113 git_commit_free( commit );
2118 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Restore successful, cleaning up" ) );
2119 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2120 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
2123 recordRestoreInHistory( repo, commit, tree, aHash );
2125 git_tree_free( tree );
2126 git_commit_free( commit );
2128 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
2137 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots =
LoadSnapshots( aProjectPath );
2139 if( snapshots.empty() )
2148 if( !selectedHash.IsEmpty() )
2155 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2158 git_repository* repo =
nullptr;
2160 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2163 git_revwalk* walk =
nullptr;
2164 if( git_revwalk_new( &walk, repo ) != 0 )
2166 git_repository_free( repo );
2170 git_revwalk_sorting( walk, GIT_SORT_TIME );
2171 git_revwalk_push_head( walk );
2175 while( git_revwalk_next( &oid, walk ) == 0 )
2177 git_commit* commit =
nullptr;
2179 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2183 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2184 info.date = wxDateTime(
static_cast<time_t
>( git_commit_time( commit ) ) );
2185 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2187 wxString firstLine =
info.message.BeforeFirst(
'\n' );
2189 long parsedCount = 0;
2191 firstLine.BeforeFirst(
':', &remainder );
2192 remainder.Trim(
true ).Trim(
false );
2194 if( remainder.EndsWith( wxS(
"files changed" ) ) )
2196 wxString countText = remainder.BeforeFirst(
' ' );
2198 if( countText.ToLong( &parsedCount ) )
2199 info.filesChanged =
static_cast<int>( parsedCount );
2202 info.summary = firstLine.BeforeFirst(
':' );
2205 info.message.BeforeFirst(
'\n', &rest );
2206 wxArrayString lines = wxSplit( rest,
'\n',
'\0' );
2208 for(
const wxString& line : lines )
2210 if( !line.IsEmpty() )
2211 info.changedFiles.Add( line );
2214 snapshots.push_back( std::move(
info ) );
2215 git_commit_free( commit );
2218 git_revwalk_free( walk );
2219 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.
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.