37#include <wx/filename.h>
41#include <wx/datetime.h>
56 wxFileName p( aProjectPath, wxEmptyString );
57 p.AppendDir( wxS(
".history" ) );
72 wxFileName fn( aFile );
74 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
82 const void* aSaverObject,
83 const std::function<
void(
const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
87 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping"), aSaverObject );
92 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
100 auto it =
m_savers.find( aSaverObject );
105 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
114 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers") );
120 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
122 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
126 wxLogTrace(
traceAutoSave, wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu"),
127 aProjectPath, aTitle,
m_savers.size() );
131 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
138 wxLogTrace(
traceAutoSave, wxS(
"[history] previous save still in progress; skipping cycle") );
143 std::vector<HISTORY_FILE_DATA> fileData;
145 for(
const auto& [saverObject, saver] :
m_savers )
147 size_t before = fileData.size();
148 saver( aProjectPath, fileData );
149 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p produced %zu entries (total=%zu)"),
150 saverObject, fileData.size() - before, fileData.size() );
154 wxString projectDir = aProjectPath;
155 if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
156 projectDir += wxFileName::GetPathSeparator();
158 auto it = std::remove_if( fileData.begin(), fileData.end(),
161 if( !entry.path.StartsWith( projectDir ) )
163 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out entry outside project: %s"), entry.path );
168 fileData.erase( it, fileData.end() );
170 if( fileData.empty() )
172 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no entries; skipping") );
177 m_saveInProgress.store(
true, std::memory_order_release );
180 [
this, projectPath = aProjectPath, title = aTitle,
181 data = std::move( fileData )]()
mutable ->
bool
183 bool result = commitInBackground( projectPath, title, data );
184 m_saveInProgress.store(
false, std::memory_order_release );
193 const std::vector<HISTORY_FILE_DATA>& aFileData )
195 wxLogTrace(
traceAutoSave, wxS(
"[history] background: writing %zu entries for '%s'"),
196 aFileData.size(), aProjectPath );
203 if( !entry.content.empty() )
205 std::string buf = entry.content;
210 wxFFile fp( entry.path, wxS(
"wb" ) );
214 fp.Write( buf.data(), buf.size() );
216 wxLogTrace(
traceAutoSave, wxS(
"[history] background: wrote %zu bytes to '%s'"),
217 buf.size(), entry.path );
221 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to open '%s' for writing"),
225 else if( !entry.sourcePath.IsEmpty() )
227 wxCopyFile( entry.sourcePath, entry.path,
true );
228 wxLogTrace(
traceAutoSave, wxS(
"[history] background: copied '%s' -> '%s'"),
229 entry.sourcePath, entry.path );
248 wxFileName src( entry.path );
250 if( !src.FileExists() )
253 if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
255 std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
256 git_index_add_bypath(
index, relHist.c_str() );
262 git_commit* head_commit =
nullptr;
263 git_tree* head_tree =
nullptr;
265 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
266 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
267 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
269 git_tree* rawIndexTree =
nullptr;
270 git_oid index_tree_oid;
272 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
274 if( head_tree ) git_tree_free( head_tree );
275 if( head_commit ) git_commit_free( head_commit );
276 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to write index tree" ) );
280 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
281 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
283 bool hasChanges =
true;
287 git_diff* diff =
nullptr;
289 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
291 hasChanges = git_diff_num_deltas( diff ) > 0;
292 wxLogTrace(
traceAutoSave, wxS(
"[history] background: diff deltas=%u"), (
unsigned) git_diff_num_deltas( diff ) );
293 git_diff_free( diff );
297 if( head_tree ) git_tree_free( head_tree );
298 if( head_commit ) git_commit_free( head_commit );
302 wxLogTrace(
traceAutoSave, wxS(
"[history] background: no changes detected; no commit") );
306 git_signature* rawSig =
nullptr;
308 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
310 git_commit* parent =
nullptr;
314 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
316 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
320 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
322 const git_commit* constParent = parent;
324 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
325 msg.mb_str().data(), indexTree.get(), parents,
326 parents ? &constParent :
nullptr );
329 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit created %s (%s entries=%zu)"),
330 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
332 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit failed rc=%d"), rc );
334 if( parent ) git_commit_free( parent );
336 git_index_write(
index );
345 wxLogTrace(
traceAutoSave, wxS(
"[history] waiting for pending background save") );
361 if( aProjectPath.IsEmpty() )
364 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
369 if( !wxDirExists( hist ) )
371 if( wxIsWritable( aProjectPath ) )
373 if( !wxMkdir( hist ) )
380 git_repository* rawRepo =
nullptr;
381 bool isNewRepo =
false;
383 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
385 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
390 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
391 if( !ignoreFile.FileExists() )
393 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
396 f.Write( wxS(
"fp-info-cache\n*-backups\nREADME.txt\n" ) );
401 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
403 if( !readmeFile.FileExists() )
405 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
409 f.Write( wxS(
"KiCad Local History Directory\n"
410 "=============================\n\n"
411 "This directory contains automatic snapshots of your project files.\n"
412 "KiCad periodically saves copies of your work here, allowing you to\n"
413 "recover from accidental changes or data loss.\n\n"
414 "You can browse and restore previous versions through KiCad's\n"
415 "File > Local History menu.\n\n"
416 "To disable this feature:\n"
417 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
418 "This directory can be safely deleted if you no longer need the\n"
419 "history, but doing so will permanently remove all saved snapshots.\n" ) );
425 git_repository_free( rawRepo );
431 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: New repository created, collecting existing files" ) );
435 std::function<void(
const wxString& )> collect = [&](
const wxString&
path )
443 bool cont = d.GetFirst( &
name );
448 if(
name.StartsWith( wxS(
"." ) ) ||
name.EndsWith( wxS(
"-backups" ) ) )
450 cont = d.GetNext( &
name );
455 wxString fullPath = fn.GetFullPath();
457 if( wxFileName::DirExists( fullPath ) )
461 else if( fn.FileExists() )
464 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
465 files.Add( fn.GetFullPath() );
468 cont = d.GetNext( &
name );
472 collect( aProjectPath );
474 if( files.GetCount() > 0 )
476 std::vector<wxString> vec;
477 vec.reserve( files.GetCount() );
479 for(
unsigned i = 0; i < files.GetCount(); ++i )
480 vec.push_back( files[i] );
482 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: Creating initial snapshot with %zu files" ), vec.size() );
487 TagSave( aProjectPath, wxS(
"project" ) );
491 wxLogTrace(
traceAutoSave, wxS(
"[history] Init: No files found to add to initial snapshot" ) );
509 const wxString& aHistoryPath,
const wxString& aProjectPath,
510 const std::vector<wxString>& aFiles,
const wxString& aTitle )
512 std::vector<std::string> filesArrStr;
514 for(
const wxString& file : aFiles )
516 wxFileName src( file );
519 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
520 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
522 relPath = src.GetFullName();
524 relPath.Replace(
"\\",
"/" );
525 std::string relPathStr = relPath.ToStdString();
527 unsigned int status = 0;
528 int rc = git_status_file( &status, repo, relPathStr.data() );
530 if( rc == 0 && status != 0 )
532 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
533 filesArrStr.emplace_back( relPathStr );
537 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
538 filesArrStr.emplace_back( relPathStr );
542 std::vector<char*> cStrings( filesArrStr.size() );
544 for(
size_t i = 0; i < filesArrStr.size(); i++ )
545 cStrings[i] = filesArrStr[i].data();
547 git_strarray filesArrGit;
548 filesArrGit.count = filesArrStr.size();
549 filesArrGit.strings = cStrings.data();
551 if( filesArrStr.size() == 0 )
557 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
559 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
565 if( git_index_write_tree( &tree_id,
index ) != 0 )
568 git_tree* rawTree =
nullptr;
569 git_tree_lookup( &rawTree, repo, &tree_id );
570 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
572 git_signature* rawSig =
nullptr;
574 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
575 &git_signature_free );
577 git_commit* rawParent =
nullptr;
581 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
583 git_commit_lookup( &rawParent, repo, &parent_id );
587 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
590 git_tree* rawParentTree =
nullptr;
593 git_commit_tree( &rawParentTree, parent.get() );
595 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
597 git_diff* rawDiff =
nullptr;
598 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
599 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
601 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
603 if( numChangedFiles == 0 )
605 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
611 if( !aTitle.IsEmpty() )
612 msg << aTitle << wxS(
": " );
614 msg << numChangedFiles << wxS(
" files changed" );
616 for(
size_t i = 0; i < numChangedFiles; ++i )
618 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
619 git_patch* rawPatch =
nullptr;
620 git_patch_from_diff( &rawPatch, diff.get(), i );
621 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
623 size_t context = 0, adds = 0, dels = 0;
624 git_patch_line_stats( &context, &adds, &dels, patch.get() );
625 size_t updated = std::min( adds, dels );
628 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
629 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
633 git_commit* parentPtr = parent.get();
634 const git_commit* constParentPtr = parentPtr;
635 if( git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr, msg.mb_str().data(), tree.get(),
636 parents, parentPtr ? &constParentPtr :
nullptr )
642 git_index_write(
index );
649 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled )
652 wxString proj = wxFileName( aFiles[0] ).GetPath();
660 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitSnapshot failed to acquire lock: %s"),
675 wxDir dir( aProjectPath );
677 if( !dir.IsOpened() )
681 std::function<void(
const wxString&)> collect = [&](
const wxString&
path )
689 bool cont = d.GetFirst( &
name );
693 if(
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) )
695 cont = d.GetNext( &
name );
700 wxString fullPath = fn.GetFullPath();
702 if( wxFileName::DirExists( fullPath ) )
706 else if( fn.FileExists() )
709 if( fn.GetFullName() != wxS(
"fp-info-cache" ) )
710 aFiles.push_back( fn.GetFullPath() );
713 cont = d.GetNext( &
name );
717 collect( aProjectPath );
723 std::vector<wxString> files;
743 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
753 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
758 git_reference* ref =
nullptr;
761 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
762 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
765 git_object* head_obj =
nullptr;
766 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
767 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
768 git_object_free( head_obj );
771 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
772 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
774 git_reference_delete( ref );
775 git_reference_free( ref );
778 git_oid last_tag_oid;
779 git_object* head_obj2 =
nullptr;
780 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
781 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
782 git_object_free( head_obj2 );
790 git_repository* repo =
nullptr;
792 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
796 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
798 git_repository_free( repo );
802 git_commit* head_commit =
nullptr;
803 git_commit_lookup( &head_commit, repo, &head_oid );
804 git_time_t head_time = git_commit_time( head_commit );
807 git_tag_list_match( &tags,
"Last_Save_*", repo );
808 git_time_t save_time = 0;
810 for(
size_t i = 0; i < tags.count; ++i )
812 git_reference* ref =
nullptr;
813 if( git_reference_lookup( &ref, repo,
814 ( wxS(
"refs/tags/" ) +
815 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
817 const git_oid* oid = git_reference_target( ref );
818 git_commit* c =
nullptr;
819 if( git_commit_lookup( &c, repo, oid ) == 0 )
821 git_time_t t = git_commit_time( c );
824 git_commit_free( c );
826 git_reference_free( ref );
830 git_strarray_free( &tags );
831 git_commit_free( head_commit );
832 git_repository_free( repo );
839 return head_time > save_time;
843 const wxString& aMessage )
849 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
858 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
859 git_reference* lastRef =
nullptr;
860 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
862 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
864 const git_oid* lastOid = git_reference_target( lastRef );
865 git_commit* lastCommit =
nullptr;
866 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
868 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
870 git_tree* lastTree =
nullptr;
871 git_commit_tree( &lastTree, lastCommit );
872 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
876 git_commit* headCommit =
nullptr;
878 const git_commit* parentArray[1];
879 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
880 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
882 parentArray[0] = headCommit;
886 git_signature* sigRaw =
nullptr;
888 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
890 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
891 git_oid newCommitOid;
892 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
893 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
894 if( headCommit ) git_commit_free( headCommit );
899 git_reference* existing =
nullptr;
900 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
902 git_reference_delete( existing );
903 git_reference_free( existing );
905 git_object* newCommitObj =
nullptr;
906 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
908 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
909 git_object_free( newCommitObj );
918 if( !dir.IsOpened() )
921 bool cont = dir.GetFirst( &
name );
925 wxString fullPath = fn.GetFullPath();
927 if( wxFileName::DirExists( fullPath ) )
929 else if( fn.FileExists() )
930 total += (size_t) fn.GetSize().GetValue();
931 cont = dir.GetNext( &
name );
943 if( !wxDirExists( hist ) )
948 if( current <= aMaxBytes )
955 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
965 git_revwalk* walk =
nullptr;
966 git_revwalk_new( &walk, repo );
967 git_revwalk_sorting( walk, GIT_SORT_TIME );
968 git_revwalk_push_head( walk );
969 std::vector<git_oid> commits;
972 while( git_revwalk_next( &oid, walk ) == 0 )
973 commits.push_back( oid );
975 git_revwalk_free( walk );
977 if( commits.empty() )
981 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
982 [](
const git_oid& a,
const git_oid& b )
984 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
987 size_t keptBytes = 0;
988 std::vector<git_oid> keep;
990 git_odb* odb =
nullptr;
991 git_repository_odb( &odb, repo );
993 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
996 size_t cnt = git_tree_entrycount( tree );
998 for(
size_t i = 0; i < cnt; ++i )
1000 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1002 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1004 const git_oid* bid = git_tree_entry_id( entry );
1006 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1009 git_object_t type = GIT_OBJECT_ANY;
1011 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1014 seenBlobs.insert( *bid );
1017 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1019 git_tree* sub =
nullptr;
1021 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1023 added += accountTree( sub );
1024 git_tree_free( sub );
1032 for(
const git_oid& cOid : commits )
1034 git_commit* c =
nullptr;
1036 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1039 git_tree* tree =
nullptr;
1040 git_commit_tree( &tree, c );
1041 size_t add = accountTree( tree );
1042 git_tree_free( tree );
1043 git_commit_free( c );
1045 if( keep.empty() || keptBytes + add <= aMaxBytes )
1047 keep.push_back( cOid );
1054 git_odb_free( odb );
1057 keep.push_back( commits.front() );
1061 std::vector<std::pair<wxString, git_oid>> tagTargets;
1062 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
1063 [](
const git_oid& a,
const git_oid& b )
1065 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1067 git_strarray tagList;
1069 if( git_tag_list( &tagList, repo ) == 0 )
1071 for(
size_t i = 0; i < tagList.count; ++i )
1073 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1074 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1076 git_reference* tref =
nullptr;
1078 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1080 const git_oid* toid = git_reference_target( tref );
1084 tagTargets.emplace_back(
name, *toid );
1085 taggedCommits.insert( *toid );
1089 for(
const auto& k : keep )
1091 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1101 keep.push_back( *toid );
1102 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1107 git_reference_free( tref );
1111 git_strarray_free( &tagList );
1115 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1116 wxString trimPath = trimFn.GetPath();
1118 if( wxDirExists( trimPath ) )
1119 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1121 wxMkdir( trimPath );
1122 git_repository* newRepo =
nullptr;
1124 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1128 std::reverse( keep.begin(), keep.end() );
1129 git_commit* parent =
nullptr;
1130 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1131 std::vector<MAP_ENTRY> commitMap;
1133 for(
const git_oid& co : keep )
1135 git_commit* orig =
nullptr;
1137 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1140 git_tree* tree =
nullptr;
1141 git_commit_tree( &tree, orig );
1145 wxArrayString toDelete;
1146 wxDir d( trimPath );
1148 bool cont = d.GetFirst( &nm );
1151 if( nm != wxS(
".git") )
1153 cont = d.GetNext( &nm );
1156 for(
auto& del : toDelete )
1158 wxFileName f( trimPath, del );
1160 wxFileName::Rmdir( f.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1161 else if( f.FileExists() )
1162 wxRemoveFile( f.GetFullPath() );
1166 std::function<void(git_tree*,
const wxString&)> writeTree = [&]( git_tree* t,
const wxString& base )
1168 size_t ecnt = git_tree_entrycount( t );
1169 for(
size_t i = 0; i < ecnt; ++i )
1171 const git_tree_entry* e = git_tree_entry_byindex( t, i );
1172 wxString
name = wxString::FromUTF8( git_tree_entry_name( e ) );
1174 if( git_tree_entry_type( e ) == GIT_OBJECT_TREE )
1176 wxFileName dir( base,
name );
1177 wxMkdir( dir.GetFullPath() );
1178 git_tree* sub =
nullptr;
1180 if( git_tree_lookup( &sub, repo, git_tree_entry_id( e ) ) == 0 )
1182 writeTree( sub, dir.GetFullPath() );
1183 git_tree_free( sub );
1186 else if( git_tree_entry_type( e ) == GIT_OBJECT_BLOB )
1188 git_blob* blob =
nullptr;
1190 if( git_blob_lookup( &blob, repo, git_tree_entry_id( e ) ) == 0 )
1192 wxFileName file( base,
name );
1193 wxFFile f( file.GetFullPath(), wxT(
"wb") );
1197 f.Write( (
const char*) git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1200 git_blob_free( blob );
1206 writeTree( tree, trimPath );
1208 git_index* newIndex =
nullptr;
1209 git_repository_index( &newIndex, newRepo );
1210 git_index_add_all( newIndex,
nullptr, 0,
nullptr,
nullptr );
1211 git_index_write( newIndex );
1213 git_index_write_tree( &newTreeOid, newIndex );
1214 git_tree* newTree =
nullptr;
1215 git_tree_lookup( &newTree, newRepo, &newTreeOid );
1218 const git_signature* origAuthor = git_commit_author( orig );
1219 const git_signature* origCommitter = git_commit_committer( orig );
1220 git_signature* sigAuthor =
nullptr;
1221 git_signature* sigCommitter =
nullptr;
1223 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1224 origAuthor->when.time, origAuthor->when.offset );
1225 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1226 origCommitter->when.time, origCommitter->when.offset );
1228 const git_commit* parents[1];
1229 int parentCount = 0;
1233 parents[0] = parent;
1237 git_oid newCommitOid;
1238 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1239 newTree, parentCount, parentCount ? parents :
nullptr );
1241 git_commit_free( parent );
1243 git_commit_lookup( &parent, newRepo, &newCommitOid );
1245 commitMap.emplace_back( co, newCommitOid );
1247 git_signature_free( sigAuthor );
1248 git_signature_free( sigCommitter );
1249 git_tree_free( newTree );
1250 git_index_free( newIndex );
1251 git_tree_free( tree );
1252 git_commit_free( orig );
1256 git_commit_free( parent );
1259 for(
const auto& tt : tagTargets )
1262 const git_oid* newOid =
nullptr;
1264 for(
const auto& m : commitMap )
1266 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1276 git_object* obj =
nullptr;
1278 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1280 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1281 git_object_free( obj );
1288 git_repository_free( newRepo );
1291 wxString backupOld = hist + wxS(
"_old");
1292 wxRenameFile( hist, backupOld );
1293 wxRenameFile( trimPath, hist );
1294 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1301 git_repository* repo =
nullptr;
1303 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1304 return wxEmptyString;
1307 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1309 git_repository_free( repo );
1310 return wxEmptyString;
1313 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1314 git_repository_free( repo );
1326bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1328 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
1330 wxDir dir( dirPath );
1331 if( !dir.IsOpened() )
1335 bool cont = dir.GetFirst( &filename );
1339 wxFileName fullPath( dirPath, filename );
1342 if( filename == wxS(
".history") || filename == wxS(
".git") )
1344 cont = dir.GetNext( &filename );
1348 if( fullPath.DirExists() )
1350 findLocks( fullPath.GetFullPath() );
1352 else if( fullPath.FileExists()
1359 baseName = baseName.BeforeLast(
'.' );
1360 wxFileName originalFile( dirPath, baseName );
1363 LOCKFILE testLock( originalFile.GetFullPath() );
1364 if( testLock.Valid() && !testLock.IsLockedByMe() )
1366 aLockedFiles.push_back( fullPath.GetFullPath() );
1370 cont = dir.GetNext( &filename );
1374 findLocks( aProjectPath );
1375 return aLockedFiles.empty();
1382bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
1384 bool extractSuccess =
true;
1386 std::function<void( git_tree*,
const wxString& )> extractTree =
1387 [&]( git_tree* t,
const wxString& prefix )
1389 if( !extractSuccess )
1392 size_t cnt = git_tree_entrycount( t );
1393 for(
size_t i = 0; i < cnt; ++i )
1395 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1396 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1397 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
1399 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1401 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
1403 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1406 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
1407 dirPath.GetPath() );
1408 extractSuccess =
false;
1412 git_tree* sub =
nullptr;
1413 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
1415 extractTree( sub, fullPath );
1416 git_tree_free( sub );
1419 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1421 git_blob* blob =
nullptr;
1422 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
1424 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
1426 wxFileName dstDir( dst );
1427 dstDir.SetFullName( wxEmptyString );
1428 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1430 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
1433 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1439 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
1440 dst.GetFullPath() );
1441 extractSuccess =
false;
1442 git_blob_free( blob );
1446 git_blob_free( blob );
1452 extractTree( aTree, wxEmptyString );
1453 return extractSuccess;
1460void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
1461 std::set<wxString>& aFiles )
1463 wxDir dir( aSearchPath );
1464 if( !dir.IsOpened() )
1468 bool cont = dir.GetFirst( &filename );
1472 wxFileName fullPath( aSearchPath, filename );
1473 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
1475 if( fullPath.IsDir() && fullPath.DirExists() )
1477 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
1479 else if( fullPath.FileExists() )
1481 aFiles.insert( relativePath );
1484 cont = dir.GetNext( &filename );
1492bool shouldExcludeFromBackup(
const wxString& aFilename )
1495 return aFilename == wxS(
"fp-info-cache" );
1502void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
1503 std::vector<wxString>& aFilesToDelete )
1505 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
1506 [&](
const wxString& dirPath,
const wxString& relativeBase )
1508 wxDir dir( dirPath );
1509 if( !dir.IsOpened() )
1513 bool cont = dir.GetFirst( &filename );
1518 if( filename == wxS(
".history") || filename == wxS(
".git") ||
1519 filename == wxS(
"_restore_backup") || filename == wxS(
"_restore_temp") )
1521 cont = dir.GetNext( &filename );
1525 wxFileName fullPath( dirPath, filename );
1526 wxString relativePath = relativeBase.IsEmpty() ? filename :
1527 relativeBase + wxS(
"/") + filename;
1529 if( fullPath.IsDir() && fullPath.DirExists() )
1531 scanDirectory( fullPath.GetFullPath(), relativePath );
1533 else if( fullPath.FileExists() )
1536 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
1539 if( !shouldExcludeFromBackup( filename ) )
1540 aFilesToDelete.push_back( relativePath );
1544 cont = dir.GetNext( &filename );
1548 scanDirectory( aProjectPath, wxEmptyString );
1556bool confirmFileDeletion( wxWindow* aParent,
const std::vector<wxString>& aFilesToDelete,
1557 bool& aKeepAllFiles )
1559 if( aFilesToDelete.empty() || !aParent )
1561 aKeepAllFiles =
false;
1565 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
1568 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
1569 for(
size_t i = 0; i < displayCount; ++i )
1571 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
1574 if( aFilesToDelete.size() > displayCount )
1576 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
1577 aFilesToDelete.size() - displayCount );
1581 wxYES_NO | wxCANCEL | wxICON_QUESTION );
1582 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
1583 dlg.SetExtendedMessage(
1584 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
1585 "files in the project directory. Choosing 'Proceed' will delete files that are not "
1586 "present in the restored commit." ) );
1588 int choice = dlg.ShowModal();
1590 if( choice == wxID_CANCEL )
1592 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
1595 else if( choice == wxID_NO )
1597 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
1598 aKeepAllFiles =
true;
1602 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
1603 aKeepAllFiles =
false;
1613bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
1614 const wxString& aTempRestorePath,
bool aKeepAllFiles,
1615 std::set<wxString>& aBackedUpFiles )
1617 wxDir currentDir( aProjectPath );
1618 if( !currentDir.IsOpened() )
1622 bool cont = currentDir.GetFirst( &filename );
1626 if( filename != wxS(
".history" ) && filename != wxS(
".git" ) &&
1627 filename != wxS(
"_restore_backup" ) && filename != wxS(
"_restore_temp" ) )
1630 bool shouldBackup = !aKeepAllFiles;
1635 wxFileName testPath( aTempRestorePath, filename );
1636 shouldBackup = testPath.Exists();
1641 wxFileName source( aProjectPath, filename );
1642 wxFileName dest( aBackupPath, filename );
1645 if( !wxDirExists( aBackupPath ) )
1648 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
1650 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
1654 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
1655 source.GetFullPath(), dest.GetFullPath() );
1657 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1660 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
1661 source.GetFullPath() );
1665 aBackedUpFiles.insert( filename );
1668 cont = currentDir.GetNext( &filename );
1678bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
1679 std::set<wxString>& aRestoredFiles )
1681 wxDir tempDir( aTempRestorePath );
1682 if( !tempDir.IsOpened() )
1686 bool cont = tempDir.GetFirst( &filename );
1690 wxFileName source( aTempRestorePath, filename );
1691 wxFileName dest( aProjectPath, filename );
1694 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
1695 source.GetFullPath(), dest.GetFullPath() );
1697 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1700 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
1701 source.GetFullPath() );
1705 aRestoredFiles.insert( filename );
1706 cont = tempDir.GetNext( &filename );
1716void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
1717 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
1718 const std::set<wxString>& aRestoredFiles )
1720 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
1724 for(
const wxString& filename : aRestoredFiles )
1726 wxFileName toRemove( aProjectPath, filename );
1727 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
1728 toRemove.GetFullPath() );
1730 if( toRemove.DirExists() )
1732 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1734 else if( toRemove.FileExists() )
1736 wxRemoveFile( toRemove.GetFullPath() );
1741 if( wxDirExists( aBackupPath ) )
1743 for(
const wxString& filename : aBackedUpFiles )
1745 wxFileName source( aBackupPath, filename );
1746 wxFileName dest( aProjectPath, filename );
1748 if( source.Exists() )
1750 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1751 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
1752 dest.GetFullPath() );
1758 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
1759 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
1766bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
1767 const wxString& aHash )
1769 git_time_t t = git_commit_time( aCommit );
1770 wxDateTime dt( (time_t) t );
1771 git_signature* sig =
nullptr;
1773 git_commit* parent =
nullptr;
1776 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
1777 git_commit_lookup( &parent, aRepo, &parent_id );
1780 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1783 const git_commit* constParent = parent;
1784 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
1785 msg.mb_str().data(), aTree, parent ? 1 : 0,
1786 parent ? &constParent :
nullptr );
1789 git_commit_free( parent );
1790 git_signature_free( sig );
1802 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
1805 std::vector<wxString> lockedFiles;
1806 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
1809 for(
const auto& f : lockedFiles )
1810 lockList += wxS(
"\n - ") + f;
1813 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
1819 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
1821 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
1832 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
1843 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
1845 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
1849 git_commit* commit =
nullptr;
1850 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
1852 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
1856 git_tree* tree =
nullptr;
1857 git_commit_tree( &tree, commit );
1860 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
1862 std::vector<wxString> backupFiles;
1865 if( !backupFiles.empty() )
1869 backupFiles, wxS(
"Pre-restore backup" ) );
1874 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
1875 git_tree_free( tree );
1876 git_commit_free( commit );
1882 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Current state already matches HEAD; "
1883 "continuing without a new backup commit" ) );
1888 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
1890 if( wxDirExists( tempRestorePath ) )
1891 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1893 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
1896 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
1898 git_tree_free( tree );
1899 git_commit_free( commit );
1903 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
1906 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
1908 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
1909 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1910 git_tree_free( tree );
1911 git_commit_free( commit );
1916 std::set<wxString> restoredFiles;
1917 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
1919 std::vector<wxString> filesToDelete;
1920 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
1922 bool keepAllFiles =
true;
1923 if( !confirmFileDeletion( aParent, filesToDelete, keepAllFiles ) )
1926 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1927 git_tree_free( tree );
1928 git_commit_free( commit );
1933 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
1935 wxString backupPath = aProjectPath + wxS(
"_restore_backup");
1938 if( wxDirExists( backupPath ) )
1940 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Removing old backup %s" ),
1942 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1946 std::set<wxString> backedUpFiles;
1947 std::set<wxString> restoredFilesSet;
1950 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
1953 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1955 git_tree_free( tree );
1956 git_commit_free( commit );
1961 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
1963 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1965 git_tree_free( tree );
1966 git_commit_free( commit );
1971 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Restore successful, cleaning up" ) );
1972 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1973 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1976 recordRestoreInHistory( repo, commit, tree, aHash );
1978 git_tree_free( tree );
1979 git_commit_free( commit );
1981 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
1990 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots =
LoadSnapshots( aProjectPath );
1992 if( snapshots.empty() )
2001 if( !selectedHash.IsEmpty() )
2008 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2011 git_repository* repo =
nullptr;
2013 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2016 git_revwalk* walk =
nullptr;
2017 if( git_revwalk_new( &walk, repo ) != 0 )
2019 git_repository_free( repo );
2023 git_revwalk_push_head( walk );
2027 while( git_revwalk_next( &oid, walk ) == 0 )
2029 git_commit* commit =
nullptr;
2031 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2035 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2036 info.date = wxDateTime(
static_cast<time_t
>( git_commit_time( commit ) ) );
2037 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2039 wxString firstLine =
info.message.BeforeFirst(
'\n' );
2041 long parsedCount = 0;
2043 firstLine.BeforeFirst(
':', &remainder );
2044 remainder.Trim(
true ).Trim(
false );
2046 if( remainder.EndsWith( wxS(
"files changed" ) ) )
2048 wxString countText = remainder.BeforeFirst(
' ' );
2050 if( countText.ToLong( &parsedCount ) )
2051 info.filesChanged =
static_cast<int>( parsedCount );
2054 info.summary = firstLine.BeforeFirst(
':' );
2057 info.message.BeforeFirst(
'\n', &rest );
2058 wxArrayString lines = wxSplit( rest,
'\n',
'\0' );
2060 for(
const wxString& line : lines )
2062 if( !line.IsEmpty() )
2063 info.changedFiles.Add( line );
2066 snapshots.push_back( std::move(
info ) );
2067 git_commit_free( commit );
2070 git_revwalk_free( walk );
2071 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).
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 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 EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
void ClearAllSavers()
Clear all registered savers.
bool CommitSnapshot(const std::vector< wxString > &aFiles, const wxString &aTitle)
Commit the given files to the local history repository.
std::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.
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
static const std::string LockFileExtension
static const std::string LockFilePrefix
const wxChar *const traceAutoSave
Flag to enable auto save feature debug tracing.
static wxString historyPath(const wxString &aProjectPath)
static wxString historyPath(const wxString &aProjectPath)
static size_t dirSizeRecursive(const wxString &path)
static 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.