42#include <wx/filename.h>
46#include <wx/datetime.h>
70 const wxString& aRelativePath )
72 wxFileName fn( aRelativePath );
75 return fn.GetFullPath();
80 wxArrayString dirs = fn.GetDirs();
83 dst.AssignDir( aHistoryRoot );
85 for(
const wxString& d : dirs )
88 dst.SetFullName( fn.GetFullName() );
89 return dst.GetFullPath();
99 wxFFile fileA( aPathA, wxS(
"rb" ) );
100 wxFFile fileB( aPathB, wxS(
"rb" ) );
102 if( !fileA.IsOpened() || !fileB.IsOpened() )
105 wxFileOffset lenA = fileA.Length();
106 wxFileOffset lenB = fileB.Length();
108 if( lenA < 0 || lenB < 0 || lenA != lenB )
111 constexpr size_t chunkSize = 64 * 1024;
112 std::vector<char> bufA( chunkSize );
113 std::vector<char> bufB( chunkSize );
115 while( !fileA.Eof() )
117 size_t readA = fileA.Read( bufA.data(), chunkSize );
118 size_t readB = fileB.Read( bufB.data(), chunkSize );
123 if( readA > 0 && std::memcmp( bufA.data(), bufB.data(), readA ) != 0 )
126 if( fileA.Error() || fileB.Error() )
140 const wxString& aRelativePath,
143 wxFileName rel( aRelativePath );
145 dst.AssignDir( aAutosaveRoot );
147 for(
const wxString& d : rel.GetDirs() )
153 dst.SetFullName( rel.GetFullName() );
155 return dst.GetFullPath();
163 const wxString& aProjectPath,
164 const wxString& aAutosaveRoot,
167 wxFileName autosave( aAutosavePath );
171 wxString
name = autosave.GetFullName();
174 return wxEmptyString;
177 return autosave.GetFullPath();
180 if( !aAutosavePath.StartsWith( aAutosaveRoot ) )
181 return wxEmptyString;
183 wxString rel = aAutosavePath.Mid( aAutosaveRoot.length() );
184 wxFileName projFn( aProjectPath, wxEmptyString );
186 return projFn.GetPathWithSep() + rel;
191 const wxString& aTitle );
212 if( aProjectPath.IsEmpty() || !wxDirExists( aProjectPath ) )
215 wxDir dir( aProjectPath );
218 return dir.IsOpened()
229 return aName == wxS(
".history" )
230 || aName == wxS(
".git" )
231 || aName == wxS(
"_restore_backup" )
232 || aName.StartsWith( wxS(
"_restore_backup_" ) )
233 || aName == wxS(
"_restore_temp" )
248 wxFileName fn( aFile );
250 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
258 const void* aSaverObject,
259 const std::function<
void(
const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
263 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping" ), aSaverObject );
268 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)" ), aSaverObject,
m_savers.size() );
276 auto it =
m_savers.find( aSaverObject );
281 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)" ),
291 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers" ) );
296 const wxString& aTagFileType )
298 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
300 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
306 wxLogTrace(
traceAutoSave, wxS(
"[history] Backup format is ZIP; skipping git commit" ) );
313 Init( aProjectPath );
316 wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu tag='%s'" ),
317 aProjectPath, aTitle,
m_savers.size(), aTagFileType );
321 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
326 if( !aTagFileType.IsEmpty() )
332 wxLogTrace(
traceAutoSave, wxS(
"[history] previous save still in progress; skipping cycle" ) );
337 std::vector<HISTORY_FILE_DATA> fileData;
339 for(
const auto& [saverObject, saver] :
m_savers )
341 size_t before = fileData.size();
342 saver( aProjectPath, fileData );
343 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p produced %zu entries (total=%zu)" ),
344 saverObject, fileData.size() - before, fileData.size() );
350 fileData.erase( std::remove_if( fileData.begin(), fileData.end(),
353 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
355 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out entry with invalid path: '%s'" ),
356 entry.relativePath );
363 if( fileData.empty() )
365 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no entries; skipping" ) );
370 m_saveInProgress.store(
true, std::memory_order_release );
373 [
this, projectPath = aProjectPath, title = aTitle, tagFileType = aTagFileType,
374 data = std::move( fileData )]()
mutable ->
bool
376 bool result = commitInBackground( projectPath, title, data, !tagFileType.IsEmpty() );
378 if( !tagFileType.IsEmpty() )
379 TagSave( projectPath, tagFileType );
381 m_saveInProgress.store(
false, std::memory_order_release );
386 if( !aTagFileType.IsEmpty() )
387 WaitForPendingSave();
395 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
400 wxLogTrace(
traceAutoSave, wxS(
"[autosave] no savers registered; skipping" ) );
410 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create autosave root '%s'" ), autosaveRoot );
414 std::vector<HISTORY_FILE_DATA> fileData;
416 for(
const auto& [saverObject, saver] :
m_savers )
417 saver( aProjectPath, fileData );
419 bool anyWritten =
false;
423 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
427 wxFileName dstFn( dst );
431 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create dir '%s'" ), dstFn.GetPath() );
437 if( !entry.content.empty() )
439 buf = std::move( entry.content );
444 else if( !entry.sourcePath.IsEmpty() )
446 wxFFile src( entry.sourcePath, wxS(
"rb" ) );
448 if( !src.IsOpened() )
451 wxFileOffset len = src.Length();
456 buf.resize(
static_cast<size_t>( len ) );
458 if( len > 0 && src.Read( buf.data(), buf.size() ) != buf.size() )
474 wxLogTrace(
traceAutoSave, wxS(
"[autosave] wrote %zu bytes to '%s'" ), buf.size(), dst );
478 wxLogTrace(
traceAutoSave, wxS(
"[autosave] write failed for '%s': %s" ), dst, err );
491static std::vector<std::pair<wxString, wxString>>
494 std::vector<std::pair<wxString, wxString>> results;
500 if( !wxDirExists( autosaveRoot ) )
503 std::function<void(
const wxString& )> walk = [&](
const wxString& aDir )
511 bool cont = d.GetFirst( &
name );
515 wxFileName fn( aDir,
name );
516 wxString fullPath = fn.GetFullPath();
518 if( wxDirExists( fullPath ) )
521 && (
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) ) )
523 cont = d.GetNext( &
name );
536 results.emplace_back( fullPath, src );
539 cont = d.GetNext( &
name );
543 walk( autosaveRoot );
548std::vector<std::pair<wxString, wxString>>
551 std::vector<std::pair<wxString, wxString>> results;
553 if( aExtensions.empty() )
558 wxFileName srcFn( pair.second );
561 for(
const wxString& ext : aExtensions )
563 if( srcFn.GetExt().IsSameAs( ext,
false ) )
575 if( srcFn.FileExists() )
576 srcTime = srcFn.GetModificationTime();
578 wxDateTime autosaveTime = wxFileName( pair.first ).GetModificationTime();
582 bool stale = !srcTime.IsValid()
583 || ( autosaveTime.IsLaterThan( srcTime )
587 results.emplace_back( std::move( pair ) );
601 if( wxFileExists( autosavePath ) )
602 wxRemoveFile( autosavePath );
608 const std::vector<wxString>& aSourcePaths )
const
610 if( aSourcePaths.empty() )
613 std::vector<wxFileName> targets;
614 targets.reserve( aSourcePaths.size() );
616 for(
const wxString& src : aSourcePaths )
619 targets.emplace_back( src );
622 if( targets.empty() )
627 wxFileName srcFn( srcPath );
630 for(
const wxFileName& target : targets )
632 if( srcFn.SameAs( target ) )
639 if( match && wxFileExists( autosavePath ) )
640 wxRemoveFile( autosavePath );
646 const std::vector<HISTORY_FILE_DATA>& aFileData,
bool aIsManualSave )
648 wxLogTrace(
traceAutoSave, wxS(
"[history] background: writing %zu entries for '%s'" ),
649 aFileData.size(), aProjectPath );
655 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create history root '%s'" ), hist );
662 wxFileName dstFn( dst );
663 wxString parent = dstFn.GetPath();
667 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create dir '%s'" ), parent );
671 if( !entry.content.empty() )
673 std::string buf = entry.content;
678 wxFFile fp( dst, wxS(
"wb" ) );
682 fp.Write( buf.data(), buf.size() );
684 wxLogTrace(
traceAutoSave, wxS(
"[history] background: wrote %zu bytes to '%s'" ), buf.size(), dst );
688 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to open '%s' for writing" ), dst );
691 else if( !entry.sourcePath.IsEmpty() )
693 wxCopyFile( entry.sourcePath, dst,
true );
694 wxLogTrace(
traceAutoSave, wxS(
"[history] background: copied '%s' -> '%s'" ), entry.sourcePath, dst );
710 git_repository_set_workdir( repo, hist.mb_str().data(),
false );
716 wxString rel = entry.relativePath;
717 rel.Replace( wxS(
"\\" ), wxS(
"/" ) );
721 if( !wxFileExists( abs ) )
724 git_index_add_bypath(
index, rel.ToStdString().c_str() );
729 git_commit* head_commit =
nullptr;
730 git_tree* head_tree =
nullptr;
732 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
733 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
734 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
736 git_tree* rawIndexTree =
nullptr;
737 git_oid index_tree_oid;
739 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
742 git_tree_free( head_tree );
745 git_commit_free( head_commit );
747 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to write index tree" ) );
751 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
752 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
754 bool hasChanges =
true;
758 git_diff* diff =
nullptr;
760 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
762 hasChanges = git_diff_num_deltas( diff ) > 0;
763 wxLogTrace(
traceAutoSave, wxS(
"[history] background: diff deltas=%u" ),
764 (
unsigned) git_diff_num_deltas( diff ) );
765 git_diff_free( diff );
772 bool stagedMatchesDisk =
true;
776 wxString diskPath = aProjectPath + wxFileName::GetPathSeparator() + entry.relativePath;
779 if( !wxFileExists( diskPath ) || !wxFileExists( histPath ) )
781 stagedMatchesDisk =
false;
785 wxFFile diskFile( diskPath, wxT(
"rb" ) );
786 wxFFile histFile( histPath, wxT(
"rb" ) );
788 if( !diskFile.IsOpened() || !histFile.IsOpened() || diskFile.Length() != histFile.Length() )
790 stagedMatchesDisk =
false;
794 size_t len =
static_cast<size_t>( diskFile.Length() );
795 std::string diskBuf( len,
'\0' );
796 std::string histBuf( len,
'\0' );
798 if( diskFile.Read( diskBuf.data(), len ) != len
799 || histFile.Read( histBuf.data(), len ) != len
800 || diskBuf != histBuf )
802 stagedMatchesDisk =
false;
807 if( stagedMatchesDisk && !aIsManualSave )
809 wxLogTrace(
traceAutoSave, wxS(
"[history] background: first commit; staged matches disk -- skipping" ) );
815 git_tree_free( head_tree );
818 git_commit_free( head_commit );
822 wxLogTrace(
traceAutoSave, wxS(
"[history] background: no changes detected; no commit") );
826 if( !aTitle.IsEmpty() && aTitle != wxS(
"Autosave" ) )
828 git_oid head_oid_amend;
830 if( git_reference_name_to_id( &head_oid_amend, repo,
"HEAD" ) == 0 )
832 git_commit* head_commit_amend =
nullptr;
834 if( git_commit_lookup( &head_commit_amend, repo, &head_oid_amend ) == 0 )
836 wxString existingMsg = wxString::FromUTF8( git_commit_message( head_commit_amend ) );
837 existingMsg.Trim(
true ).Trim(
false );
839 if( existingMsg != aTitle )
842 int amend_rc = git_commit_amend( &amended_oid, head_commit_amend,
"HEAD",
nullptr,
nullptr,
843 nullptr, aTitle.mb_str().data(),
nullptr );
846 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amended HEAD message '%s' -> '%s'" ),
847 existingMsg, aTitle );
849 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amend failed rc=%d" ), amend_rc );
852 git_commit_free( head_commit_amend );
860 git_signature* rawSig =
nullptr;
862 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
864 git_commit* parent =
nullptr;
868 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
870 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
874 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
876 const git_commit* constParent = parent;
878 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
879 msg.mb_str().data(), indexTree.get(), parents,
880 parents ? &constParent :
nullptr );
884 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit created %s (%s entries=%zu)" ),
885 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
889 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit failed rc=%d" ), rc );
893 git_commit_free( parent );
895 git_index_write(
index );
904 wxLogTrace(
traceAutoSave, wxS(
"[history] waiting for pending background save" ) );
928 if( !wxDirExists( hist ) )
937 git_repository* rawRepo =
nullptr;
939 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
941 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
944 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
945 if( !ignoreFile.FileExists() )
947 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
950 f.Write( wxS(
"# KiCad local history exclusions. Edit to add your own rules.\n"
957 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
959 if( !readmeFile.FileExists() )
961 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
965 f.Write( wxS(
"KiCad Local History Directory\n"
966 "=============================\n\n"
967 "This directory contains automatic snapshots of your project files.\n"
968 "KiCad periodically saves copies of your work here, allowing you to\n"
969 "recover from accidental changes or data loss.\n\n"
970 "You can browse and restore previous versions through KiCad's\n"
971 "File > Local History menu.\n\n"
972 "To disable this feature:\n"
973 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
974 "This directory can be safely deleted if you no longer need the\n"
975 "history, but doing so will permanently remove all saved snapshots.\n" ) );
981 git_repository_free( rawRepo );
997 const wxString& aHistoryPath,
const wxString& aProjectPath,
998 const std::vector<wxString>& aFiles,
const wxString& aTitle )
1000 std::vector<std::string> filesArrStr;
1002 for(
const wxString& file : aFiles )
1004 wxFileName src( file );
1007 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
1008 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
1010 relPath = src.GetFullName();
1012 relPath.Replace(
"\\",
"/" );
1013 std::string relPathStr = relPath.ToStdString();
1015 unsigned int status = 0;
1016 int rc = git_status_file( &status, repo, relPathStr.data() );
1018 if( rc == 0 && status != 0 )
1020 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
1021 filesArrStr.emplace_back( relPathStr );
1025 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
1026 filesArrStr.emplace_back( relPathStr );
1030 std::vector<char*> cStrings( filesArrStr.size() );
1032 for(
size_t i = 0; i < filesArrStr.size(); i++ )
1033 cStrings[i] = filesArrStr[i].data();
1035 git_strarray filesArrGit;
1036 filesArrGit.count = filesArrStr.size();
1037 filesArrGit.strings = cStrings.data();
1039 if( filesArrStr.size() == 0 )
1045 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
1047 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
1053 if( git_index_write_tree( &tree_id,
index ) != 0 )
1056 git_tree* rawTree =
nullptr;
1057 git_tree_lookup( &rawTree, repo, &tree_id );
1058 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
1060 git_signature* rawSig =
nullptr;
1062 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
1063 &git_signature_free );
1065 git_commit* rawParent =
nullptr;
1069 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
1071 git_commit_lookup( &rawParent, repo, &parent_id );
1075 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
1078 git_tree* rawParentTree =
nullptr;
1081 git_commit_tree( &rawParentTree, parent.get() );
1083 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
1085 git_diff* rawDiff =
nullptr;
1086 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
1087 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
1089 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
1091 if( numChangedFiles == 0 )
1093 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
1099 if( !aTitle.IsEmpty() )
1100 msg << aTitle << wxS(
": " );
1102 msg << numChangedFiles << wxS(
" files changed" );
1104 for(
size_t i = 0; i < numChangedFiles; ++i )
1106 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
1107 git_patch* rawPatch =
nullptr;
1108 git_patch_from_diff( &rawPatch, diff.get(), i );
1109 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
1111 size_t context = 0, adds = 0, dels = 0;
1112 git_patch_line_stats( &context, &adds, &dels, patch.get() );
1113 size_t updated = std::min( adds, dels );
1116 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
1117 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
1121 git_commit* parentPtr = parent.get();
1122 const git_commit* constParentPtr = parentPtr;
1123 if( git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr, msg.mb_str().data(), tree.get(),
1124 parents, parentPtr ? &constParentPtr :
nullptr )
1130 git_index_write(
index );
1139 const wxString& aTitle )
1147 wxLogTrace(
traceAutoSave, wxS(
"[history] commitSnapshotForProject failed to acquire lock: %s" ),
1159 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled
1165 wxString proj = wxFileName( aFiles[0] ).GetPath();
1179 wxString
name = aFile.GetFullName();
1181 if(
name == wxS(
"sym-lib-table" ) ||
name == wxS(
"fp-lib-table" ) )
1184 return aFile.GetExt().StartsWith( wxS(
"kicad_" ) );
1193 wxDir dir( aProjectPath );
1195 if( !dir.IsOpened() )
1199 std::function<void(
const wxString&,
bool )> collect =
1200 [&](
const wxString&
path,
bool topLevel )
1205 wxS(
"[history] collectProjectFiles: Skipping nested project at %s" ),
1216 bool cont = d.GetFirst( &
name );
1222 cont = d.GetNext( &
name );
1227 wxString fullPath = fn.GetFullPath();
1229 if( wxFileName::DirExists( fullPath ) )
1231 collect( fullPath,
false );
1233 else if( fn.FileExists() && fn.GetFullName() != wxS(
"fp-info-cache" ) &&
isKiCadProjectFile( fn ) )
1235 aFiles.push_back( fn.GetFullPath() );
1238 cont = d.GetNext( &
name );
1242 collect( aProjectPath,
true );
1253 wxLogTrace(
traceAutoSave, wxS(
"[history] Backup format is ZIP; skipping full snapshot" ) );
1257 std::vector<wxString> files;
1263 Init( aProjectPath );
1269 return wxDirExists(
historyPath( aProjectPath ) );
1284 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
1294 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
1299 git_reference* ref =
nullptr;
1302 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
1303 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
1306 git_object* head_obj =
nullptr;
1307 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
1308 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
1309 git_object_free( head_obj );
1312 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
1313 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
1315 git_reference_delete( ref );
1316 git_reference_free( ref );
1319 git_oid last_tag_oid;
1320 git_object* head_obj2 =
nullptr;
1321 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
1322 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
1323 git_object_free( head_obj2 );
1331 git_repository* repo =
nullptr;
1333 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1337 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1339 git_repository_free( repo );
1343 git_commit* head_commit =
nullptr;
1344 git_commit_lookup( &head_commit, repo, &head_oid );
1345 git_time_t head_time = git_commit_time( head_commit );
1348 git_tag_list_match( &tags,
"Last_Save_*", repo );
1349 git_time_t save_time = 0;
1351 for(
size_t i = 0; i < tags.count; ++i )
1353 git_reference* ref =
nullptr;
1354 if( git_reference_lookup( &ref, repo,
1355 ( wxS(
"refs/tags/" ) +
1356 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
1358 const git_oid* oid = git_reference_target( ref );
1359 git_commit* c =
nullptr;
1360 if( git_commit_lookup( &c, repo, oid ) == 0 )
1362 git_time_t t = git_commit_time( c );
1365 git_commit_free( c );
1367 git_reference_free( ref );
1371 git_strarray_free( &tags );
1372 git_commit_free( head_commit );
1373 git_repository_free( repo );
1377 if( save_time == 0 )
1380 return head_time > save_time;
1384 const wxString& aMessage )
1396 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
1405 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
1406 git_reference* lastRef =
nullptr;
1407 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
1409 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
1411 const git_oid* lastOid = git_reference_target( lastRef );
1412 git_commit* lastCommit =
nullptr;
1413 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
1415 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
1417 git_tree* lastTree =
nullptr;
1418 git_commit_tree( &lastTree, lastCommit );
1419 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
1423 git_commit* headCommit =
nullptr;
1425 const git_commit* parentArray[1];
1426 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
1427 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
1429 parentArray[0] = headCommit;
1433 git_signature* sigRaw =
nullptr;
1435 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
1437 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
1438 git_oid newCommitOid;
1439 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
1440 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
1441 if( headCommit ) git_commit_free( headCommit );
1446 git_reference* existing =
nullptr;
1447 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
1449 git_reference_delete( existing );
1450 git_reference_free( existing );
1452 git_object* newCommitObj =
nullptr;
1453 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
1455 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
1456 git_object_free( newCommitObj );
1465 if( !dir.IsOpened() )
1468 bool cont = dir.GetFirst( &
name );
1472 wxString fullPath = fn.GetFullPath();
1474 if( wxFileName::DirExists( fullPath ) )
1476 else if( fn.FileExists() )
1477 total += (size_t) fn.GetSize().GetValue();
1478 cont = dir.GetNext( &
name );
1484static bool copyTreeObjects( git_repository* aSrcRepo, git_odb* aSrcOdb, git_odb* aDstOdb,
const git_oid* aTreeOid,
1485 std::set<git_oid,
bool ( * )(
const git_oid&,
const git_oid& )>& aCopied )
1487 if( aCopied.count( *aTreeOid ) )
1490 git_odb_object* obj =
nullptr;
1492 if( git_odb_read( &obj, aSrcOdb, aTreeOid ) != 0 )
1496 int err = git_odb_write( &written, aDstOdb, git_odb_object_data( obj ), git_odb_object_size( obj ),
1497 git_odb_object_type( obj ) );
1498 git_odb_object_free( obj );
1503 aCopied.insert( *aTreeOid );
1505 git_tree* tree =
nullptr;
1507 if( git_tree_lookup( &tree, aSrcRepo, aTreeOid ) != 0 )
1510 size_t cnt = git_tree_entrycount( tree );
1512 for(
size_t i = 0; i < cnt; ++i )
1514 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1515 const git_oid* entryId = git_tree_entry_id( entry );
1517 if( aCopied.count( *entryId ) )
1520 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1522 if( !
copyTreeObjects( aSrcRepo, aSrcOdb, aDstOdb, entryId, aCopied ) )
1524 git_tree_free( tree );
1528 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1530 git_odb_object* blobObj =
nullptr;
1532 if( git_odb_read( &blobObj, aSrcOdb, entryId ) == 0 )
1534 git_oid blobWritten;
1536 if( git_odb_write( &blobWritten, aDstOdb, git_odb_object_data( blobObj ),
1537 git_odb_object_size( blobObj ), git_odb_object_type( blobObj ) )
1540 git_odb_object_free( blobObj );
1541 git_tree_free( tree );
1545 git_odb_object_free( blobObj );
1546 aCopied.insert( *entryId );
1551 git_tree_free( tree );
1560 git_packbuilder* pb =
nullptr;
1562 if( git_packbuilder_new( &pb, aRepo ) != 0 )
1565 git_revwalk* walk =
nullptr;
1567 if( git_revwalk_new( &walk, aRepo ) != 0 )
1569 git_packbuilder_free( pb );
1573 git_revwalk_push_head( walk );
1576 while( git_revwalk_next( &oid, walk ) == 0 )
1578 if( git_packbuilder_insert_commit( pb, &oid ) != 0 )
1580 git_revwalk_free( walk );
1581 git_packbuilder_free( pb );
1586 git_revwalk_free( walk );
1590 git_packbuilder_set_callbacks(
1592 [](
int aStage, uint32_t aCurrent, uint32_t aTotal,
void* aPayload )
1597 reporter->SetCurrentProgress( (
double) aCurrent / aTotal );
1605 if( git_packbuilder_write( pb,
nullptr, 0,
nullptr,
nullptr ) != 0 )
1607 git_packbuilder_free( pb );
1611 git_packbuilder_free( pb );
1613 wxString objPath = wxString::FromUTF8( git_repository_path( aRepo ) ) + wxS(
"objects" );
1614 wxDir objDir( objPath );
1616 if( objDir.IsOpened() )
1618 wxArrayString toRemove;
1620 bool cont = objDir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS );
1624 if(
name.length() == 2 )
1625 toRemove.Add( objPath + wxFileName::GetPathSeparator() +
name );
1627 cont = objDir.GetNext( &
name );
1630 for(
const wxString& dir : toRemove )
1631 wxFileName::Rmdir( dir, wxPATH_RMDIR_RECURSIVE );
1640 if( aMaxBytes == 0 )
1645 if( !wxDirExists( hist ) )
1650 if( current <= aMaxBytes )
1657 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
1667 aReporter->
Report(
_(
"Compacting local history..." ) );
1674 if( current <= aMaxBytes )
1678 git_revwalk* walk =
nullptr;
1679 git_revwalk_new( &walk, repo );
1680 git_revwalk_sorting( walk, GIT_SORT_TIME );
1681 git_revwalk_push_head( walk );
1682 std::vector<git_oid> commits;
1685 while( git_revwalk_next( &oid, walk ) == 0 )
1686 commits.push_back( oid );
1688 git_revwalk_free( walk );
1690 if( commits.empty() )
1694 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
1695 [](
const git_oid& a,
const git_oid& b )
1697 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1700 size_t keptBytes = 0;
1701 std::vector<git_oid> keep;
1703 git_odb* odb =
nullptr;
1704 git_repository_odb( &odb, repo );
1706 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
1709 size_t cnt = git_tree_entrycount( tree );
1711 for(
size_t i = 0; i < cnt; ++i )
1713 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1715 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1717 const git_oid* bid = git_tree_entry_id( entry );
1719 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1722 git_object_t type = GIT_OBJECT_ANY;
1724 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1727 seenBlobs.insert( *bid );
1730 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1732 git_tree* sub =
nullptr;
1734 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1736 added += accountTree( sub );
1737 git_tree_free( sub );
1745 for(
const git_oid& cOid : commits )
1747 git_commit* c =
nullptr;
1749 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1752 git_tree* tree =
nullptr;
1753 git_commit_tree( &tree, c );
1754 size_t add = accountTree( tree );
1755 git_tree_free( tree );
1756 git_commit_free( c );
1758 if( keep.empty() || keptBytes + add <= aMaxBytes )
1760 keep.push_back( cOid );
1768 keep.push_back( commits.front() );
1772 std::vector<std::pair<wxString, git_oid>> tagTargets;
1773 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
1774 [](
const git_oid& a,
const git_oid& b )
1776 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1778 git_strarray tagList;
1780 if( git_tag_list( &tagList, repo ) == 0 )
1782 for(
size_t i = 0; i < tagList.count; ++i )
1784 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1785 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1787 git_reference* tref =
nullptr;
1789 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1791 const git_oid* toid = git_reference_target( tref );
1795 tagTargets.emplace_back(
name, *toid );
1796 taggedCommits.insert( *toid );
1800 for(
const auto& k : keep )
1802 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1812 keep.push_back( *toid );
1813 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1818 git_reference_free( tref );
1822 git_strarray_free( &tagList );
1826 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1827 wxString trimPath = trimFn.GetPath();
1829 if( wxDirExists( trimPath ) )
1830 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1832 wxMkdir( trimPath );
1833 git_repository* newRepo =
nullptr;
1835 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1837 git_odb_free( odb );
1841 git_odb* dstOdb =
nullptr;
1843 if( git_repository_odb( &dstOdb, newRepo ) != 0 )
1845 git_repository_free( newRepo );
1846 git_odb_free( odb );
1850 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> copiedObjects(
1851 [](
const git_oid& a,
const git_oid& b )
1853 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1857 std::reverse( keep.begin(), keep.end() );
1858 git_commit* parent =
nullptr;
1859 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1860 std::vector<MAP_ENTRY> commitMap;
1864 aReporter->
AdvancePhase(
_(
"Trimming local history..." ) );
1868 for(
size_t idx = 0; idx < keep.size(); ++idx )
1873 const git_oid& co = keep[idx];
1874 git_commit* orig =
nullptr;
1876 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1879 git_tree* tree =
nullptr;
1880 git_commit_tree( &tree, orig );
1882 copyTreeObjects( repo, odb, dstOdb, git_tree_id( tree ), copiedObjects );
1884 git_tree* newTree =
nullptr;
1885 git_tree_lookup( &newTree, newRepo, git_tree_id( tree ) );
1887 git_tree_free( tree );
1890 const git_signature* origAuthor = git_commit_author( orig );
1891 const git_signature* origCommitter = git_commit_committer( orig );
1892 git_signature* sigAuthor =
nullptr;
1893 git_signature* sigCommitter =
nullptr;
1895 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1896 origAuthor->when.time, origAuthor->when.offset );
1897 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1898 origCommitter->when.time, origCommitter->when.offset );
1900 const git_commit* parents[1];
1901 int parentCount = 0;
1905 parents[0] = parent;
1909 git_oid newCommitOid;
1910 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1911 newTree, parentCount, parentCount ? parents :
nullptr );
1914 git_commit_free( parent );
1916 git_commit_lookup( &parent, newRepo, &newCommitOid );
1918 commitMap.emplace_back( co, newCommitOid );
1920 git_signature_free( sigAuthor );
1921 git_signature_free( sigCommitter );
1922 git_tree_free( newTree );
1923 git_commit_free( orig );
1927 git_commit_free( parent );
1930 for(
const auto& tt : tagTargets )
1933 const git_oid* newOid =
nullptr;
1935 for(
const auto& m : commitMap )
1937 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1947 git_object* obj =
nullptr;
1949 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1951 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1952 git_object_free( obj );
1957 aReporter->
AdvancePhase(
_(
"Compacting trimmed history..." ) );
1964 git_odb_free( dstOdb );
1965 git_odb_free( odb );
1966 git_repository_free( newRepo );
1971 wxString backupOld = hist + wxS(
"_old");
1972 wxRenameFile( hist, backupOld );
1973 wxRenameFile( trimPath, hist );
1974 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1981 git_repository* repo =
nullptr;
1983 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1984 return wxEmptyString;
1987 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1989 git_repository_free( repo );
1990 return wxEmptyString;
1993 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1994 git_repository_free( repo );
2006bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
2008 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
2010 wxDir dir( dirPath );
2011 if( !dir.IsOpened() )
2015 bool cont = dir.GetFirst( &filename );
2019 wxFileName fullPath( dirPath, filename );
2022 if( filename == wxS(
".history") || filename == wxS(
".git") )
2024 cont = dir.GetNext( &filename );
2028 if( fullPath.DirExists() )
2030 findLocks( fullPath.GetFullPath() );
2032 else if( fullPath.FileExists()
2039 baseName = baseName.BeforeLast(
'.' );
2040 wxFileName originalFile( dirPath, baseName );
2043 LOCKFILE testLock( originalFile.GetFullPath() );
2044 if( testLock.Valid() && !testLock.IsLockedByMe() )
2046 aLockedFiles.push_back( fullPath.GetFullPath() );
2050 cont = dir.GetNext( &filename );
2054 findLocks( aProjectPath );
2055 return aLockedFiles.empty();
2062bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
2064 bool extractSuccess =
true;
2066 std::function<void( git_tree*,
const wxString& )> extractTree =
2067 [&]( git_tree* t,
const wxString& prefix )
2069 if( !extractSuccess )
2072 size_t cnt = git_tree_entrycount( t );
2073 for(
size_t i = 0; i < cnt; ++i )
2075 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
2076 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
2077 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
2079 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
2081 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
2083 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
2086 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
2087 dirPath.GetPath() );
2088 extractSuccess =
false;
2092 git_tree* sub =
nullptr;
2093 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
2095 extractTree( sub, fullPath );
2096 git_tree_free( sub );
2099 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
2101 git_blob* blob =
nullptr;
2102 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
2104 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
2106 wxFileName dstDir( dst );
2107 dstDir.SetFullName( wxEmptyString );
2108 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
2110 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
2113 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
2119 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
2120 dst.GetFullPath() );
2121 extractSuccess =
false;
2122 git_blob_free( blob );
2126 git_blob_free( blob );
2132 extractTree( aTree, wxEmptyString );
2133 return extractSuccess;
2140void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
2141 std::set<wxString>& aFiles )
2143 wxDir dir( aSearchPath );
2144 if( !dir.IsOpened() )
2148 bool cont = dir.GetFirst( &filename );
2152 wxFileName fullPath( aSearchPath, filename );
2153 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
2155 if( fullPath.IsDir() && fullPath.DirExists() )
2157 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
2159 else if( fullPath.FileExists() )
2161 aFiles.insert( relativePath );
2164 cont = dir.GetNext( &filename );
2172bool shouldExcludeFromBackup(
const wxString& aFilename )
2179bool isPathUnderNestedProject(
const wxString& aProjectPath,
const wxString& aRelativePath )
2181 if( aRelativePath.IsEmpty() )
2184 wxArrayString parts = wxSplit( aRelativePath,
'/',
'\0' );
2186 if( parts.GetCount() < 2 )
2189 wxString accumulated = aProjectPath;
2193 for(
size_t i = 0; i + 1 < parts.GetCount(); ++i )
2195 accumulated += wxFileName::GetPathSeparator() + parts[i];
2208void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
2209 std::vector<wxString>& aFilesToDelete )
2211 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
2212 [&](
const wxString& dirPath,
const wxString& relativeBase )
2214 wxDir dir( dirPath );
2215 if( !dir.IsOpened() )
2219 bool cont = dir.GetFirst( &filename );
2227 cont = dir.GetNext( &filename );
2231 wxFileName fullPath( dirPath, filename );
2232 wxString relativePath = relativeBase.IsEmpty() ? filename :
2233 relativeBase + wxS(
"/") + filename;
2235 if( fullPath.IsDir() && fullPath.DirExists() )
2242 wxS(
"[history] findFilesToDelete: Skipping nested project "
2244 fullPath.GetFullPath() );
2248 scanDirectory( fullPath.GetFullPath(), relativePath );
2251 else if( fullPath.FileExists() )
2254 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
2257 if( !shouldExcludeFromBackup( filename ) )
2258 aFilesToDelete.push_back( relativePath );
2262 cont = dir.GetNext( &filename );
2266 scanDirectory( aProjectPath, wxEmptyString );
2274bool confirmFileDeletion( wxWindow* aParent,
const wxString& aProjectPath,
2275 const wxString& aBackupPath,
2276 const std::vector<wxString>& aFilesToDelete,
bool& aKeepAllFiles )
2278 if( aFilesToDelete.empty() || !aParent )
2280 aKeepAllFiles =
true;
2284 bool hasNestedProjectFile =
false;
2286 for(
const wxString& rel : aFilesToDelete )
2288 if( isPathUnderNestedProject( aProjectPath, rel ) )
2290 hasNestedProjectFile =
true;
2295 if( hasNestedProjectFile )
2297 wxLogTrace(
traceAutoSave, wxS(
"[history] Forcing keepAllFiles due to nested project under "
2298 "candidate path" ) );
2299 aKeepAllFiles =
true;
2303 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
2306 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
2307 for(
size_t i = 0; i < displayCount; ++i )
2309 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
2312 if( aFilesToDelete.size() > displayCount )
2314 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
2315 aFilesToDelete.size() - displayCount );
2319 wxYES_NO | wxCANCEL | wxNO_DEFAULT | wxICON_QUESTION );
2320 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
2321 dlg.SetExtendedMessage(
2322 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
2323 "files in the project directory. Choosing 'Proceed' will delete files that are not "
2324 "present in the restored commit." )
2326 + wxString::Format(
_(
"Files removed by 'Proceed' are archived to %s and can be "
2327 "recovered manually." ),
2330 int choice = dlg.ShowModal();
2332 if( choice == wxID_CANCEL )
2334 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
2337 else if( choice == wxID_NO )
2339 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
2340 aKeepAllFiles =
true;
2344 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
2345 aKeepAllFiles =
false;
2355bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
2356 const wxString& aTempRestorePath,
bool aKeepAllFiles,
2357 std::set<wxString>& aBackedUpFiles )
2359 wxDir currentDir( aProjectPath );
2360 if( !currentDir.IsOpened() )
2364 bool cont = currentDir.GetFirst( &filename );
2373 bool shouldBackup = !aKeepAllFiles;
2378 wxFileName testPath( aTempRestorePath, filename );
2379 shouldBackup = testPath.Exists();
2384 wxFileName source( aProjectPath, filename );
2385 wxFileName dest( aBackupPath, filename );
2388 if( !wxDirExists( aBackupPath ) )
2391 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
2393 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
2397 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
2398 source.GetFullPath(), dest.GetFullPath() );
2400 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
2403 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
2404 source.GetFullPath() );
2408 aBackedUpFiles.insert( filename );
2411 cont = currentDir.GetNext( &filename );
2421bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
2422 std::set<wxString>& aRestoredFiles )
2424 wxDir tempDir( aTempRestorePath );
2425 if( !tempDir.IsOpened() )
2429 bool cont = tempDir.GetFirst( &filename );
2433 wxFileName source( aTempRestorePath, filename );
2434 wxFileName dest( aProjectPath, filename );
2437 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
2438 source.GetFullPath(), dest.GetFullPath() );
2440 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
2443 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
2444 source.GetFullPath() );
2448 aRestoredFiles.insert( filename );
2449 cont = tempDir.GetNext( &filename );
2459void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
2460 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
2461 const std::set<wxString>& aRestoredFiles )
2463 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
2467 for(
const wxString& filename : aRestoredFiles )
2469 wxFileName toRemove( aProjectPath, filename );
2470 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
2471 toRemove.GetFullPath() );
2473 if( toRemove.DirExists() )
2475 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
2477 else if( toRemove.FileExists() )
2479 wxRemoveFile( toRemove.GetFullPath() );
2484 if( wxDirExists( aBackupPath ) )
2486 for(
const wxString& filename : aBackedUpFiles )
2488 wxFileName source( aBackupPath, filename );
2489 wxFileName dest( aProjectPath, filename );
2491 if( source.Exists() )
2493 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
2494 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
2495 dest.GetFullPath() );
2501 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
2502 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
2509bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
2510 const wxString& aHash )
2512 git_time_t t = git_commit_time( aCommit );
2513 wxDateTime dt( (time_t) t );
2514 git_signature* sig =
nullptr;
2516 git_commit* parent =
nullptr;
2519 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
2520 git_commit_lookup( &parent, aRepo, &parent_id );
2523 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
2526 const git_commit* constParent = parent;
2527 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
2528 msg.mb_str().data(), aTree, parent ? 1 : 0,
2529 parent ? &constParent :
nullptr );
2532 git_commit_free( parent );
2533 git_signature_free( sig );
2545 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
2548 std::vector<wxString> lockedFiles;
2549 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
2552 for(
const auto& f : lockedFiles )
2553 lockList += wxS(
"\n - ") + f;
2556 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
2562 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
2564 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
2575 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
2586 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
2588 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
2592 git_commit* commit =
nullptr;
2593 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2595 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
2599 git_tree* tree =
nullptr;
2600 git_commit_tree( &tree, commit );
2603 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
2605 std::vector<wxString> backupFiles;
2608 if( !backupFiles.empty() )
2612 backupFiles, wxS(
"Pre-restore backup" ) );
2617 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
2618 git_tree_free( tree );
2619 git_commit_free( commit );
2625 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Current state already matches HEAD; "
2626 "continuing without a new backup commit" ) );
2631 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
2633 if( wxDirExists( tempRestorePath ) )
2634 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2636 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
2639 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
2641 git_tree_free( tree );
2642 git_commit_free( commit );
2646 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
2649 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
2651 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
2652 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2653 git_tree_free( tree );
2654 git_commit_free( commit );
2659 std::set<wxString> restoredFiles;
2660 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
2662 std::vector<wxString> filesToDelete;
2663 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
2670 wxString backupPath =
2671 aProjectPath + wxS(
"_restore_backup_" )
2672 + wxDateTime::UNow().Format( wxS(
"%Y-%m-%dT%H-%M-%S-%l" ) );
2674 bool keepAllFiles =
true;
2675 if( !confirmFileDeletion( aParent, aProjectPath, backupPath, filesToDelete, keepAllFiles ) )
2678 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2679 git_tree_free( tree );
2680 git_commit_free( commit );
2685 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
2688 std::set<wxString> backedUpFiles;
2689 std::set<wxString> restoredFilesSet;
2692 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
2695 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2697 git_tree_free( tree );
2698 git_commit_free( commit );
2703 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
2705 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2707 git_tree_free( tree );
2708 git_commit_free( commit );
2714 wxS(
"[history] RestoreCommit: Restore successful, backup retained at %s" ),
2716 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2719 recordRestoreInHistory( repo, commit, tree, aHash );
2721 git_tree_free( tree );
2722 git_commit_free( commit );
2724 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
2733 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots =
LoadSnapshots( aProjectPath );
2735 if( snapshots.empty() )
2744 if( !selectedHash.IsEmpty() )
2751 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2754 git_repository* repo =
nullptr;
2756 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2759 git_revwalk* walk =
nullptr;
2760 if( git_revwalk_new( &walk, repo ) != 0 )
2762 git_repository_free( repo );
2766 git_revwalk_sorting( walk, GIT_SORT_TIME );
2767 git_revwalk_push_head( walk );
2771 while( git_revwalk_next( &oid, walk ) == 0 )
2773 git_commit* commit =
nullptr;
2775 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2779 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2780 info.date = wxDateTime(
static_cast<time_t
>( git_commit_time( commit ) ) );
2781 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2783 wxString firstLine =
info.message.BeforeFirst(
'\n' );
2785 long parsedCount = 0;
2787 firstLine.BeforeFirst(
':', &remainder );
2788 remainder.Trim(
true ).Trim(
false );
2790 if( remainder.EndsWith( wxS(
"files changed" ) ) )
2792 wxString countText = remainder.BeforeFirst(
' ' );
2794 if( countText.ToLong( &parsedCount ) )
2795 info.filesChanged =
static_cast<int>( parsedCount );
2798 info.summary = firstLine.BeforeFirst(
':' );
2801 info.message.BeforeFirst(
'\n', &rest );
2802 wxArrayString lines = wxSplit( rest,
'\n',
'\0' );
2804 for(
const wxString& line : lines )
2806 if( !line.IsEmpty() )
2807 info.changedFiles.Add( line );
2810 snapshots.push_back( std::move(
info ) );
2811 git_commit_free( commit );
2814 git_revwalk_free( walk );
2815 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.
bool RunRegisteredSaversAndCommit(const wxString &aProjectPath, const wxString &aTitle, const wxString &aTagFileType=wxEmptyString)
Run all registered savers and, if any staged changes differ from HEAD, create a commit.
std::vector< std::pair< wxString, wxString > > FindStaleAutosaveFiles(const wxString &aProjectPath, const std::vector< wxString > &aExtensions) const
Enumerate autosave files newer than their corresponding source files for the project at aProjectPath,...
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.
bool commitInBackground(const wxString &aProjectPath, const wxString &aTitle, const std::vector< HISTORY_FILE_DATA > &aFileData, bool aIsManualSave)
Execute file writes and git commit on a background thread.
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...
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
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 RunRegisteredSaversAsAutosaveFiles(const wxString &aProjectPath)
Run all registered savers and write their output to autosave files instead of committing to the local...
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.
void RemoveAutosaveFiles(const wxString &aProjectPath) const
Remove every autosave file under the project at aProjectPath regardless of which source it shadowed.
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
virtual COMMON_SETTINGS * GetCommonSettings() const
virtual SETTINGS_MANAGER & GetSettingsManager() const
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).
COMMON_SETTINGS * GetCommonSettings() const
Retrieve the common settings shared by all applications.
wxString GetAutosaveRootForProject(const PROJECT *aProject=nullptr) const
Resolve the autosave-files root for a project.
PROJECT * GetProjectForPath(const wxString &aProjectPath) const
Return the active project iff its path matches aProjectPath, else nullptr.
wxString GetLocalHistoryDirForPath(const wxString &aProjectPath) const
Resolve the local-history directory for a project given by its on-disk path.
@ INCREMENTAL
Git-based local history (default)
@ PROJECT_DIR
Inside the project directory (default)
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
static const std::string LockFileExtension
static const std::string ProjectFileExtension
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 bool isRestoreProtectedEntry(const wxString &aName)
static std::vector< std::pair< wxString, wxString > > findAutosaveFilePairs(const wxString &aProjectPath)
static const wxString AUTOSAVE_PREFIX
static bool commitSnapshotForProject(const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
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 bool isKiCadProjectFile(const wxFileName &aFile)
static wxString sourceForAutosaveFile(const wxString &aAutosavePath, const wxString &aProjectPath, const wxString &aAutosaveRoot, BACKUP_LOCATION aLocation)
static wxString resolveAutosaveDestination(const wxString &aAutosaveRoot, const wxString &aRelativePath, BACKUP_LOCATION aLocation)
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 bool filesContentEqual(const wxString &aPathA, const wxString &aPathB)
static bool formatUsesIncrementalHistory()
static bool isProjectDirectory(const wxString &aProjectPath)
static void collectProjectFiles(const wxString &aProjectPath, std::vector< wxString > &aFiles)
static wxString joinHistoryDestination(const wxString &aHistoryRoot, const wxString &aRelativePath)
PGM_BASE & Pgm()
The global program "get" accessor.
#define PROJECT_BACKUPS_DIR_SUFFIX
Project settings path will be <projectname> + this.
BACKUP_LOCATION location
Where backups, history, and autosave files live.
BACKUP_FORMAT format
Backup format (incremental git history vs zip archives)
Data produced by a registered saver on the UI thread, consumed by either the background local-history...
IbisParser parser & reporter
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.