40#include <wx/filename.h>
44#include <wx/datetime.h>
68 const wxString& aRelativePath )
70 wxFileName fn( aRelativePath );
73 return fn.GetFullPath();
78 wxArrayString dirs = fn.GetDirs();
81 dst.AssignDir( aHistoryRoot );
83 for(
const wxString& d : dirs )
86 dst.SetFullName( fn.GetFullName() );
87 return dst.GetFullPath();
97 wxFFile fileA( aPathA, wxS(
"rb" ) );
98 wxFFile fileB( aPathB, wxS(
"rb" ) );
100 if( !fileA.IsOpened() || !fileB.IsOpened() )
103 wxFileOffset lenA = fileA.Length();
104 wxFileOffset lenB = fileB.Length();
106 if( lenA < 0 || lenB < 0 || lenA != lenB )
109 constexpr size_t chunkSize = 64 * 1024;
110 std::vector<char> bufA( chunkSize );
111 std::vector<char> bufB( chunkSize );
113 while( !fileA.Eof() )
115 size_t readA = fileA.Read( bufA.data(), chunkSize );
116 size_t readB = fileB.Read( bufB.data(), chunkSize );
121 if( readA > 0 && std::memcmp( bufA.data(), bufB.data(), readA ) != 0 )
124 if( fileA.Error() || fileB.Error() )
138 const wxString& aRelativePath,
141 wxFileName rel( aRelativePath );
143 dst.AssignDir( aAutosaveRoot );
145 for(
const wxString& d : rel.GetDirs() )
151 dst.SetFullName( rel.GetFullName() );
153 return dst.GetFullPath();
161 const wxString& aProjectPath,
162 const wxString& aAutosaveRoot,
165 wxFileName autosave( aAutosavePath );
169 wxString
name = autosave.GetFullName();
172 return wxEmptyString;
175 return autosave.GetFullPath();
178 if( !aAutosavePath.StartsWith( aAutosaveRoot ) )
179 return wxEmptyString;
181 wxString rel = aAutosavePath.Mid( aAutosaveRoot.length() );
182 wxFileName projFn( aProjectPath, wxEmptyString );
184 return projFn.GetPathWithSep() + rel;
189 const wxString& aTitle );
210 if( aProjectPath.IsEmpty() || !wxDirExists( aProjectPath ) )
213 wxDir dir( aProjectPath );
216 return dir.IsOpened()
227 return aName == wxS(
".history" ) || aName == wxS(
".history_old" )
228 || aName.StartsWith( wxS(
".history_old_" ) )
229 || aName == wxS(
".git" ) || aName == wxS(
"_restore_backup" )
230 || aName.StartsWith( wxS(
"_restore_backup_" ) ) || aName == wxS(
"_restore_temp" )
245 wxFileName fn( aFile );
255 const void* aSaverObject,
256 const std::function<
void(
const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver,
257 const std::weak_ptr<void>& aLifetime )
261 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping" ), aSaverObject );
266 entry.
saver = aSaver;
271 entry.
tracked = aLifetime.lock() !=
nullptr;
273 m_savers[aSaverObject] = std::move( entry );
274 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)" ), aSaverObject,
m_savers.size() );
280 std::vector<const void*> expired;
284 for(
const auto& [saverObject, entry] :
m_savers )
286 if( entry.tracked && entry.lifetime.expired() )
287 expired.push_back( saverObject );
290 for(
const void* obj : expired )
299 auto it =
m_savers.find( aSaverObject );
304 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)" ),
314 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers" ) );
319 const wxString& aTagFileType )
323 wxLogTrace(
traceAutoSave, wxS(
"Local history disabled, returning" ) );
330 Init( aProjectPath );
333 wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu tag='%s'" ),
334 aProjectPath, aTitle,
m_savers.size(), aTagFileType );
338 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
343 if( !aTagFileType.IsEmpty() )
349 wxLogTrace(
traceAutoSave, wxS(
"[history] previous save still in progress; skipping cycle" ) );
356 std::vector<HISTORY_FILE_DATA> fileData;
358 for(
const auto& [saverObject, entry] :
m_savers )
360 size_t before = fileData.size();
361 entry.saver( aProjectPath, fileData );
362 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p produced %zu entries (total=%zu)" ),
363 saverObject, fileData.size() - before, fileData.size() );
369 fileData.erase( std::remove_if( fileData.begin(), fileData.end(),
372 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
374 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out entry with invalid path: '%s'" ),
375 entry.relativePath );
382 if( fileData.empty() )
384 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no entries; skipping" ) );
389 m_saveInProgress.store(
true, std::memory_order_release );
392 [
this, projectPath = aProjectPath, title = aTitle, tagFileType = aTagFileType,
393 data = std::move( fileData )]()
mutable ->
bool
395 bool result = commitInBackground( projectPath, title, data, !tagFileType.IsEmpty() );
397 if( !tagFileType.IsEmpty() )
398 TagSave( projectPath, tagFileType );
400 m_saveInProgress.store(
false, std::memory_order_release );
405 if( !aTagFileType.IsEmpty() )
406 WaitForPendingSave();
414 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
419 wxLogTrace(
traceAutoSave, wxS(
"[autosave] no savers registered; skipping" ) );
429 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create autosave root '%s'" ), autosaveRoot );
435 std::vector<HISTORY_FILE_DATA> fileData;
437 for(
const auto& [saverObject, entry] :
m_savers )
438 entry.saver( aProjectPath, fileData );
440 bool anyWritten =
false;
444 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
448 wxFileName dstFn( dst );
452 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create dir '%s'" ), dstFn.GetPath() );
458 if( !entry.content.empty() )
460 buf = std::move( entry.content );
465 else if( !entry.sourcePath.IsEmpty() )
467 wxFFile src( entry.sourcePath, wxS(
"rb" ) );
469 if( !src.IsOpened() )
472 wxFileOffset len = src.Length();
477 buf.resize(
static_cast<size_t>( len ) );
479 if( len > 0 && src.Read( buf.data(), buf.size() ) != buf.size() )
495 wxLogTrace(
traceAutoSave, wxS(
"[autosave] wrote %zu bytes to '%s'" ), buf.size(), dst );
499 wxLogTrace(
traceAutoSave, wxS(
"[autosave] write failed for '%s': %s" ), dst, err );
512std::vector<std::pair<wxString, wxString>>
516 std::vector<std::pair<wxString, wxString>> results;
518 if( !wxDirExists( aAutosaveRoot ) )
526 std::function<void(
const wxString& )> walk =
527 [&](
const wxString& aDir )
535 bool cont = d.GetFirst( &
name );
539 wxFileName fn( aDir,
name );
540 wxString fullPath = fn.GetFullPath();
542 if( wxDirExists( fullPath ) )
545 && (
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) ) )
547 cont = d.GetNext( &
name );
561 results.emplace_back( fullPath, src );
564 cont = d.GetNext( &
name );
568 walk( aAutosaveRoot );
573static std::vector<std::pair<wxString, wxString>>
584std::vector<std::pair<wxString, wxString>>
587 std::vector<std::pair<wxString, wxString>> results;
589 if( aExtensions.empty() )
594 wxFileName srcFn( pair.second );
597 for(
const wxString& ext : aExtensions )
599 if( srcFn.GetExt().IsSameAs( ext,
false ) )
611 if( srcFn.FileExists() )
612 srcTime = srcFn.GetModificationTime();
614 wxDateTime autosaveTime = wxFileName( pair.first ).GetModificationTime();
618 bool stale = !srcTime.IsValid()
619 || ( autosaveTime.IsLaterThan( srcTime )
623 results.emplace_back( std::move( pair ) );
637 if( wxFileExists( autosavePath ) )
638 wxRemoveFile( autosavePath );
644 const std::vector<wxString>& aSourcePaths )
const
646 if( aSourcePaths.empty() )
649 std::vector<wxFileName> targets;
650 targets.reserve( aSourcePaths.size() );
652 for(
const wxString& src : aSourcePaths )
655 targets.emplace_back( src );
658 if( targets.empty() )
663 wxFileName srcFn( srcPath );
666 for(
const wxFileName& target : targets )
668 if( srcFn.SameAs( target ) )
675 if( match && wxFileExists( autosavePath ) )
676 wxRemoveFile( autosavePath );
682 const std::vector<HISTORY_FILE_DATA>& aFileData,
bool aIsManualSave )
684 wxLogTrace(
traceAutoSave, wxS(
"[history] background: writing %zu entries for '%s'" ),
685 aFileData.size(), aProjectPath );
691 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create history root '%s'" ), hist );
698 wxFileName dstFn( dst );
699 wxString parent = dstFn.GetPath();
703 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create dir '%s'" ), parent );
707 if( !entry.content.empty() )
709 std::string buf = entry.content;
714 wxFFile fp( dst, wxS(
"wb" ) );
718 fp.Write( buf.data(), buf.size() );
720 wxLogTrace(
traceAutoSave, wxS(
"[history] background: wrote %zu bytes to '%s'" ), buf.size(), dst );
724 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to open '%s' for writing" ), dst );
727 else if( !entry.sourcePath.IsEmpty() )
729 wxCopyFile( entry.sourcePath, dst,
true );
730 wxLogTrace(
traceAutoSave, wxS(
"[history] background: copied '%s' -> '%s'" ), entry.sourcePath, dst );
746 git_repository_set_workdir( repo, hist.mb_str().data(),
false );
752 wxString rel = entry.relativePath;
753 rel.Replace( wxS(
"\\" ), wxS(
"/" ) );
757 if( !wxFileExists( abs ) )
760 git_index_add_bypath(
index, rel.ToStdString().c_str() );
765 git_commit* head_commit =
nullptr;
766 git_tree* head_tree =
nullptr;
768 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
769 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
770 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
772 git_tree* rawIndexTree =
nullptr;
773 git_oid index_tree_oid;
775 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
778 git_tree_free( head_tree );
781 git_commit_free( head_commit );
783 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to write index tree" ) );
787 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
788 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
790 bool hasChanges =
true;
794 git_diff* diff =
nullptr;
796 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
798 hasChanges = git_diff_num_deltas( diff ) > 0;
799 wxLogTrace(
traceAutoSave, wxS(
"[history] background: diff deltas=%u" ),
800 (
unsigned) git_diff_num_deltas( diff ) );
801 git_diff_free( diff );
808 bool stagedMatchesDisk =
true;
812 wxString diskPath = aProjectPath + wxFileName::GetPathSeparator() + entry.relativePath;
815 if( !wxFileExists( diskPath ) || !wxFileExists( histPath ) )
817 stagedMatchesDisk =
false;
821 wxFFile diskFile( diskPath, wxT(
"rb" ) );
822 wxFFile histFile( histPath, wxT(
"rb" ) );
824 if( !diskFile.IsOpened() || !histFile.IsOpened() || diskFile.Length() != histFile.Length() )
826 stagedMatchesDisk =
false;
830 size_t len =
static_cast<size_t>( diskFile.Length() );
831 std::string diskBuf( len,
'\0' );
832 std::string histBuf( len,
'\0' );
834 if( diskFile.Read( diskBuf.data(), len ) != len
835 || histFile.Read( histBuf.data(), len ) != len
836 || diskBuf != histBuf )
838 stagedMatchesDisk =
false;
843 if( stagedMatchesDisk && !aIsManualSave )
845 wxLogTrace(
traceAutoSave, wxS(
"[history] background: first commit; staged matches disk -- skipping" ) );
851 git_tree_free( head_tree );
854 git_commit_free( head_commit );
858 wxLogTrace(
traceAutoSave, wxS(
"[history] background: no changes detected; no commit") );
862 if( !aTitle.IsEmpty() && aTitle != wxS(
"Autosave" ) )
864 git_oid head_oid_amend;
866 if( git_reference_name_to_id( &head_oid_amend, repo,
"HEAD" ) == 0 )
868 git_commit* head_commit_amend =
nullptr;
870 if( git_commit_lookup( &head_commit_amend, repo, &head_oid_amend ) == 0 )
872 wxString existingMsg = wxString::FromUTF8( git_commit_message( head_commit_amend ) );
873 existingMsg.Trim(
true ).Trim(
false );
875 if( existingMsg != aTitle )
878 int amend_rc = git_commit_amend( &amended_oid, head_commit_amend,
"HEAD",
nullptr,
nullptr,
879 nullptr, aTitle.mb_str().data(),
nullptr );
882 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amended HEAD message '%s' -> '%s'" ),
883 existingMsg, aTitle );
885 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amend failed rc=%d" ), amend_rc );
888 git_commit_free( head_commit_amend );
896 git_signature* rawSig =
nullptr;
898 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
900 git_commit* parent =
nullptr;
904 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
906 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
910 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
912 const git_commit* constParent = parent;
914 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
915 msg.mb_str().data(), indexTree.get(), parents,
916 parents ? &constParent :
nullptr );
920 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit created %s (%s entries=%zu)" ),
921 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
925 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit failed rc=%d" ), rc );
929 git_commit_free( parent );
931 git_index_write(
index );
940 wxLogTrace(
traceAutoSave, wxS(
"[history] waiting for pending background save" ) );
964 if( !wxDirExists( hist ) )
966 wxLogNull suppressSysErrorPopups;
975 git_repository* rawRepo =
nullptr;
977 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
979 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
982 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
983 if( !ignoreFile.FileExists() )
985 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
988 f.Write( wxS(
"# KiCad local history exclusions. Edit to add your own rules.\n"
995 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
997 if( !readmeFile.FileExists() )
999 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
1003 f.Write( wxS(
"KiCad Local History Directory\n"
1004 "=============================\n\n"
1005 "This directory contains automatic snapshots of your project files.\n"
1006 "KiCad periodically saves copies of your work here, allowing you to\n"
1007 "recover from accidental changes or data loss.\n\n"
1008 "You can browse and restore previous versions through KiCad's\n"
1009 "File > Local History menu.\n\n"
1010 "To disable this feature:\n"
1011 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
1012 "This directory can be safely deleted if you no longer need the\n"
1013 "history, but doing so will permanently remove all saved snapshots.\n" ) );
1019 git_repository_free( rawRepo );
1035 const wxString& aHistoryPath,
const wxString& aProjectPath,
1036 const std::vector<wxString>& aFiles,
const wxString& aTitle )
1038 std::vector<std::string> filesArrStr;
1040 for(
const wxString& file : aFiles )
1042 wxFileName src( file );
1045 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
1046 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
1048 relPath = src.GetFullName();
1050 relPath.Replace(
"\\",
"/" );
1051 std::string relPathStr = relPath.ToStdString();
1053 unsigned int status = 0;
1054 int rc = git_status_file( &status, repo, relPathStr.data() );
1056 if( rc == 0 && status != 0 )
1058 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
1059 filesArrStr.emplace_back( relPathStr );
1063 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
1064 filesArrStr.emplace_back( relPathStr );
1068 std::vector<char*> cStrings( filesArrStr.size() );
1070 for(
size_t i = 0; i < filesArrStr.size(); i++ )
1071 cStrings[i] = filesArrStr[i].data();
1073 git_strarray filesArrGit;
1074 filesArrGit.count = filesArrStr.size();
1075 filesArrGit.strings = cStrings.data();
1077 if( filesArrStr.size() == 0 )
1083 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
1085 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
1091 if( git_index_write_tree( &tree_id,
index ) != 0 )
1094 git_tree* rawTree =
nullptr;
1095 git_tree_lookup( &rawTree, repo, &tree_id );
1096 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
1098 git_signature* rawSig =
nullptr;
1100 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
1101 &git_signature_free );
1103 git_commit* rawParent =
nullptr;
1107 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
1109 git_commit_lookup( &rawParent, repo, &parent_id );
1113 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
1116 git_tree* rawParentTree =
nullptr;
1119 git_commit_tree( &rawParentTree, parent.get() );
1121 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
1123 git_diff* rawDiff =
nullptr;
1124 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
1125 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
1127 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
1129 if( numChangedFiles == 0 )
1131 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
1137 if( !aTitle.IsEmpty() )
1138 msg << aTitle << wxS(
": " );
1140 msg << numChangedFiles << wxS(
" files changed" );
1142 for(
size_t i = 0; i < numChangedFiles; ++i )
1144 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
1145 git_patch* rawPatch =
nullptr;
1146 git_patch_from_diff( &rawPatch, diff.get(), i );
1147 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
1149 size_t context = 0, adds = 0, dels = 0;
1150 git_patch_line_stats( &context, &adds, &dels, patch.get() );
1151 size_t updated = std::min( adds, dels );
1154 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
1155 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
1159 git_commit* parentPtr = parent.get();
1160 const git_commit* constParentPtr = parentPtr;
1161 if( git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr, msg.mb_str().data(), tree.get(),
1162 parents, parentPtr ? &constParentPtr :
nullptr )
1168 git_index_write(
index );
1177 const wxString& aTitle )
1185 wxLogTrace(
traceAutoSave, wxS(
"[history] commitSnapshotForProject failed to acquire lock: %s" ),
1202 wxString proj = wxFileName( aFiles[0] ).GetPath();
1216 wxString
name = aFile.GetFullName();
1218 if(
name == wxS(
"sym-lib-table" ) ||
name == wxS(
"fp-lib-table" ) )
1221 return aFile.GetExt().StartsWith( wxS(
"kicad_" ) );
1230 wxDir dir( aProjectPath );
1232 if( !dir.IsOpened() )
1243 std::function<void(
const wxString&,
bool )> collect =
1244 [&](
const wxString&
path,
bool topLevel )
1249 wxS(
"[history] collectProjectFiles: Skipping nested project at %s" ),
1260 bool cont = d.GetFirst( &
name );
1266 cont = d.GetNext( &
name );
1271 wxString fullPath = fn.GetFullPath();
1273 if( wxFileName::DirExists( fullPath ) )
1276 collect( fullPath,
false );
1278 else if( fn.FileExists() && fn.GetFullName() != wxS(
"fp-info-cache" ) &&
isKiCadProjectFile( fn ) )
1280 aFiles.push_back( fn.GetFullPath() );
1283 cont = d.GetNext( &
name );
1287 collect( aProjectPath,
true );
1296 std::vector<wxString> files;
1302 Init( aProjectPath );
1308 return wxDirExists(
historyPath( aProjectPath ) );
1320 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
1325 git_reference* ref =
nullptr;
1328 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
1329 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
1332 git_object* head_obj =
nullptr;
1333 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
1334 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
1335 git_object_free( head_obj );
1338 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
1339 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
1341 git_reference_delete( ref );
1342 git_reference_free( ref );
1345 git_oid last_tag_oid;
1346 git_object* head_obj2 =
nullptr;
1347 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
1348 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
1349 git_object_free( head_obj2 );
1367 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
1377 git_repository* repo =
nullptr;
1379 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1383 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1385 git_repository_free( repo );
1389 git_commit* head_commit =
nullptr;
1390 git_commit_lookup( &head_commit, repo, &head_oid );
1391 git_time_t head_time = git_commit_time( head_commit );
1394 git_tag_list_match( &tags,
"Last_Save_*", repo );
1395 git_time_t save_time = 0;
1397 for(
size_t i = 0; i < tags.count; ++i )
1399 git_reference* ref =
nullptr;
1400 if( git_reference_lookup( &ref, repo,
1401 ( wxS(
"refs/tags/" ) +
1402 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
1404 const git_oid* oid = git_reference_target( ref );
1405 git_commit* c =
nullptr;
1406 if( git_commit_lookup( &c, repo, oid ) == 0 )
1408 git_time_t t = git_commit_time( c );
1411 git_commit_free( c );
1413 git_reference_free( ref );
1417 git_strarray_free( &tags );
1418 git_commit_free( head_commit );
1419 git_repository_free( repo );
1423 if( save_time == 0 )
1426 return head_time > save_time;
1430 const wxString& aMessage )
1442 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
1451 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
1452 git_reference* lastRef =
nullptr;
1453 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
1455 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
1457 const git_oid* lastOid = git_reference_target( lastRef );
1458 git_commit* lastCommit =
nullptr;
1459 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
1461 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
1463 git_tree* lastTree =
nullptr;
1464 git_commit_tree( &lastTree, lastCommit );
1465 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
1469 git_commit* headCommit =
nullptr;
1471 const git_commit* parentArray[1];
1472 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
1473 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
1475 parentArray[0] = headCommit;
1479 git_signature* sigRaw =
nullptr;
1481 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
1483 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
1484 git_oid newCommitOid;
1485 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
1486 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
1487 if( headCommit ) git_commit_free( headCommit );
1492 git_reference* existing =
nullptr;
1493 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
1495 git_reference_delete( existing );
1496 git_reference_free( existing );
1498 git_object* newCommitObj =
nullptr;
1499 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
1501 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
1502 git_object_free( newCommitObj );
1511 if( !dir.IsOpened() )
1514 bool cont = dir.GetFirst( &
name );
1518 wxString fullPath = fn.GetFullPath();
1520 if( wxFileName::DirExists( fullPath ) )
1522 else if( fn.FileExists() )
1523 total += (size_t) fn.GetSize().GetValue();
1524 cont = dir.GetNext( &
name );
1530static bool copyTreeObjects( git_repository* aSrcRepo, git_odb* aSrcOdb, git_odb* aDstOdb,
const git_oid* aTreeOid,
1531 std::set<git_oid,
bool ( * )(
const git_oid&,
const git_oid& )>& aCopied )
1533 if( aCopied.count( *aTreeOid ) )
1536 git_odb_object* obj =
nullptr;
1538 if( git_odb_read( &obj, aSrcOdb, aTreeOid ) != 0 )
1542 int err = git_odb_write( &written, aDstOdb, git_odb_object_data( obj ), git_odb_object_size( obj ),
1543 git_odb_object_type( obj ) );
1544 git_odb_object_free( obj );
1549 aCopied.insert( *aTreeOid );
1551 git_tree* tree =
nullptr;
1553 if( git_tree_lookup( &tree, aSrcRepo, aTreeOid ) != 0 )
1556 size_t cnt = git_tree_entrycount( tree );
1558 for(
size_t i = 0; i < cnt; ++i )
1560 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1561 const git_oid* entryId = git_tree_entry_id( entry );
1563 if( aCopied.count( *entryId ) )
1566 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1568 if( !
copyTreeObjects( aSrcRepo, aSrcOdb, aDstOdb, entryId, aCopied ) )
1570 git_tree_free( tree );
1574 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1576 git_odb_object* blobObj =
nullptr;
1578 if( git_odb_read( &blobObj, aSrcOdb, entryId ) == 0 )
1580 git_oid blobWritten;
1582 if( git_odb_write( &blobWritten, aDstOdb, git_odb_object_data( blobObj ),
1583 git_odb_object_size( blobObj ), git_odb_object_type( blobObj ) )
1586 git_odb_object_free( blobObj );
1587 git_tree_free( tree );
1591 git_odb_object_free( blobObj );
1592 aCopied.insert( *entryId );
1597 git_tree_free( tree );
1606 git_packbuilder* pb =
nullptr;
1608 if( git_packbuilder_new( &pb, aRepo ) != 0 )
1611 git_revwalk* walk =
nullptr;
1613 if( git_revwalk_new( &walk, aRepo ) != 0 )
1615 git_packbuilder_free( pb );
1619 git_revwalk_push_head( walk );
1622 while( git_revwalk_next( &oid, walk ) == 0 )
1624 if( git_packbuilder_insert_commit( pb, &oid ) != 0 )
1626 git_revwalk_free( walk );
1627 git_packbuilder_free( pb );
1632 git_revwalk_free( walk );
1636 git_packbuilder_set_callbacks(
1638 [](
int aStage, uint32_t aCurrent, uint32_t aTotal,
void* aPayload )
1643 reporter->SetCurrentProgress( (
double) aCurrent / aTotal );
1651 if( git_packbuilder_write( pb,
nullptr, 0,
nullptr,
nullptr ) != 0 )
1653 git_packbuilder_free( pb );
1657 git_packbuilder_free( pb );
1659 wxString objPath = wxString::FromUTF8( git_repository_path( aRepo ) ) + wxS(
"objects" );
1660 wxDir objDir( objPath );
1662 if( objDir.IsOpened() )
1664 wxArrayString toRemove;
1666 bool cont = objDir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS );
1670 if(
name.length() == 2 )
1671 toRemove.Add( objPath + wxFileName::GetPathSeparator() +
name );
1673 cont = objDir.GetNext( &
name );
1676 for(
const wxString& dir : toRemove )
1677 wxFileName::Rmdir( dir, wxPATH_RMDIR_RECURSIVE );
1686 if( aMaxBytes == 0 )
1691 if( !wxDirExists( hist ) )
1696 if( current <= aMaxBytes )
1703 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
1713 aReporter->
Report(
_(
"Compacting local history..." ) );
1720 if( current <= aMaxBytes )
1724 git_revwalk* walk =
nullptr;
1725 git_revwalk_new( &walk, repo );
1726 git_revwalk_sorting( walk, GIT_SORT_TIME );
1727 git_revwalk_push_head( walk );
1728 std::vector<git_oid> commits;
1731 while( git_revwalk_next( &oid, walk ) == 0 )
1732 commits.push_back( oid );
1734 git_revwalk_free( walk );
1736 if( commits.empty() )
1740 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
1741 [](
const git_oid& a,
const git_oid& b )
1743 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1746 size_t keptBytes = 0;
1747 std::vector<git_oid> keep;
1749 git_odb* odb =
nullptr;
1750 git_repository_odb( &odb, repo );
1752 std::function<size_t( git_tree* )> accountTree =
1753 [&]( git_tree* tree )
1756 size_t cnt = git_tree_entrycount( tree );
1758 for(
size_t i = 0; i < cnt; ++i )
1760 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1762 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1764 const git_oid* bid = git_tree_entry_id( entry );
1766 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1769 git_object_t type = GIT_OBJECT_ANY;
1771 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1774 seenBlobs.insert( *bid );
1777 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1779 git_tree* sub =
nullptr;
1781 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1783 added += accountTree( sub );
1784 git_tree_free( sub );
1792 for(
const git_oid& cOid : commits )
1794 git_commit* c =
nullptr;
1796 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1799 git_tree* tree =
nullptr;
1800 git_commit_tree( &tree, c );
1801 size_t add = accountTree( tree );
1802 git_tree_free( tree );
1803 git_commit_free( c );
1805 if( keep.empty() || keptBytes + add <= aMaxBytes )
1807 keep.push_back( cOid );
1815 keep.push_back( commits.front() );
1819 std::vector<std::pair<wxString, git_oid>> tagTargets;
1820 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
1821 [](
const git_oid& a,
const git_oid& b )
1823 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1825 git_strarray tagList;
1827 if( git_tag_list( &tagList, repo ) == 0 )
1829 for(
size_t i = 0; i < tagList.count; ++i )
1831 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1832 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1834 git_reference* tref =
nullptr;
1836 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1838 const git_oid* toid = git_reference_target( tref );
1842 tagTargets.emplace_back(
name, *toid );
1843 taggedCommits.insert( *toid );
1847 for(
const auto& k : keep )
1849 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1859 keep.push_back( *toid );
1860 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1865 git_reference_free( tref );
1869 git_strarray_free( &tagList );
1873 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1874 wxString trimPath = trimFn.GetPath();
1876 if( wxDirExists( trimPath ) )
1877 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1879 wxMkdir( trimPath );
1880 git_repository* newRepo =
nullptr;
1882 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1884 git_odb_free( odb );
1888 git_odb* dstOdb =
nullptr;
1890 if( git_repository_odb( &dstOdb, newRepo ) != 0 )
1892 git_repository_free( newRepo );
1893 git_odb_free( odb );
1897 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> copiedObjects(
1898 [](
const git_oid& a,
const git_oid& b )
1900 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1904 std::reverse( keep.begin(), keep.end() );
1905 git_commit* parent =
nullptr;
1906 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1907 std::vector<MAP_ENTRY> commitMap;
1911 aReporter->
AdvancePhase(
_(
"Trimming local history..." ) );
1915 for(
size_t idx = 0; idx < keep.size(); ++idx )
1920 const git_oid& co = keep[idx];
1921 git_commit* orig =
nullptr;
1923 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1926 git_tree* tree =
nullptr;
1927 git_commit_tree( &tree, orig );
1929 copyTreeObjects( repo, odb, dstOdb, git_tree_id( tree ), copiedObjects );
1931 git_tree* newTree =
nullptr;
1932 git_tree_lookup( &newTree, newRepo, git_tree_id( tree ) );
1934 git_tree_free( tree );
1937 const git_signature* origAuthor = git_commit_author( orig );
1938 const git_signature* origCommitter = git_commit_committer( orig );
1939 git_signature* sigAuthor =
nullptr;
1940 git_signature* sigCommitter =
nullptr;
1942 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1943 origAuthor->when.time, origAuthor->when.offset );
1944 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1945 origCommitter->when.time, origCommitter->when.offset );
1947 const git_commit* parents[1];
1948 int parentCount = 0;
1952 parents[0] = parent;
1956 git_oid newCommitOid;
1957 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1958 newTree, parentCount, parentCount ? parents :
nullptr );
1961 git_commit_free( parent );
1963 git_commit_lookup( &parent, newRepo, &newCommitOid );
1965 commitMap.emplace_back( co, newCommitOid );
1967 git_signature_free( sigAuthor );
1968 git_signature_free( sigCommitter );
1969 git_tree_free( newTree );
1970 git_commit_free( orig );
1975 git_tree* newHeadTree =
nullptr;
1976 git_index* newIndex =
nullptr;
1978 if( git_commit_tree( &newHeadTree, parent ) == 0 && git_repository_index( &newIndex, newRepo ) == 0 )
1980 git_index_read_tree( newIndex, newHeadTree );
1981 git_index_write( newIndex );
1985 git_index_free( newIndex );
1988 git_tree_free( newHeadTree );
1990 git_commit_free( parent );
1994 for(
const auto& tt : tagTargets )
1997 const git_oid* newOid =
nullptr;
1999 for(
const auto& m : commitMap )
2001 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
2011 git_object* obj =
nullptr;
2013 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
2015 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
2016 git_object_free( obj );
2021 aReporter->
AdvancePhase(
_(
"Compacting trimmed history..." ) );
2028 git_odb_free( dstOdb );
2029 git_odb_free( odb );
2030 git_repository_free( newRepo );
2035 wxString backupOld = hist + wxS(
"_old_" ) +
KIID().
AsString();
2037 if( wxFileExists( backupOld ) || wxDirExists( backupOld ) )
2040 if( !wxRenameFile( hist, backupOld,
false ) )
2043 if( !wxRenameFile( trimPath, hist,
false ) )
2045 if( !wxRenameFile( backupOld, hist,
false ) )
2046 wxLogError(
_(
"Could not restore local history '%s'. The previous history is preserved at '%s'." ),
2052 if( !wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE ) )
2053 wxLogTrace(
traceAutoSave, wxS(
"[history] Trimmed history installed; previous history retained at %s" ),
2062 git_repository* repo =
nullptr;
2064 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2065 return wxEmptyString;
2068 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
2070 git_repository_free( repo );
2071 return wxEmptyString;
2074 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
2075 git_repository_free( repo );
2087bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
2089 std::function<void(
const wxString& )> findLocks =
2090 [&](
const wxString& dirPath )
2092 wxDir dir( dirPath );
2093 if( !dir.IsOpened() )
2097 bool cont = dir.GetFirst( &filename );
2101 wxFileName fullPath( dirPath, filename );
2104 if( filename == wxS(
".history") || filename == wxS(
".git") )
2106 cont = dir.GetNext( &filename );
2110 if( fullPath.DirExists() )
2112 findLocks( fullPath.GetFullPath() );
2114 else if( fullPath.FileExists()
2121 baseName = baseName.BeforeLast(
'.' );
2122 wxFileName originalFile( dirPath, baseName );
2129 aLockedFiles.push_back( fullPath.GetFullPath() );
2133 cont = dir.GetNext( &filename );
2137 findLocks( aProjectPath );
2138 return aLockedFiles.empty();
2145bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
2147 bool extractSuccess =
true;
2149 std::function<void( git_tree*,
const wxString& )> extractTree =
2150 [&]( git_tree* t,
const wxString& prefix )
2152 if( !extractSuccess )
2155 size_t cnt = git_tree_entrycount( t );
2156 for(
size_t i = 0; i < cnt; ++i )
2158 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
2159 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
2160 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
2162 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
2164 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath, wxEmptyString );
2166 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
2169 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
2170 dirPath.GetPath() );
2171 extractSuccess =
false;
2175 git_tree* sub =
nullptr;
2177 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
2179 extractTree( sub, fullPath );
2180 git_tree_free( sub );
2183 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
2185 git_blob* blob =
nullptr;
2187 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
2189 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
2191 wxFileName dstDir( dst );
2192 dstDir.SetFullName( wxEmptyString );
2193 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
2195 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
2199 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
2205 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
2206 dst.GetFullPath() );
2207 extractSuccess =
false;
2208 git_blob_free( blob );
2212 git_blob_free( blob );
2218 extractTree( aTree, wxEmptyString );
2219 return extractSuccess;
2229void collectRelativeFiles(
const wxString& aRoot,
const wxString& aDir, std::vector<wxString>& aOut )
2233 if( !dir.IsOpened() )
2238 for(
bool cont = dir.GetFirst( &
name ); cont; cont = dir.GetNext( &
name ) )
2240 wxString full = aDir + wxFILE_SEP_PATH +
name;
2242 if( wxDirExists( full ) )
2244 collectRelativeFiles( aRoot, full, aOut );
2246 else if( wxFileExists( full ) )
2248 wxString rel = full.Mid( aRoot.length() + 1 );
2249 rel.Replace( wxS(
"\\" ), wxS(
"/" ) );
2250 aOut.push_back( rel );
2264bool overlaySnapshotFiles(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
const wxString& aBackupPath )
2266 std::vector<wxString> relPaths;
2267 collectRelativeFiles( aTempRestorePath, aTempRestorePath, relPaths );
2269 for(
const wxString& rel : relPaths )
2271 wxFileName src( aTempRestorePath + wxFILE_SEP_PATH + rel );
2272 wxFileName dst( aProjectPath + wxFILE_SEP_PATH + rel );
2274 if( dst.FileExists() )
2276 wxFileName bak( aBackupPath + wxFILE_SEP_PATH + rel );
2278 if( !wxFileName::Mkdir( bak.GetPath(), 0777, wxPATH_MKDIR_FULL )
2279 || !wxCopyFile( dst.GetFullPath(), bak.GetFullPath(),
true ) )
2285 if( !wxFileName::Mkdir( dst.GetPath(), 0777, wxPATH_MKDIR_FULL )
2286 || !wxCopyFile( src.GetFullPath(), dst.GetFullPath(),
true ) )
2303 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
2306 std::vector<wxString> lockedFiles;
2307 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
2310 for(
const auto& f : lockedFiles )
2311 lockList += wxS(
"\n - ") + f;
2314 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
2320 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
2322 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
2333 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
2344 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
2346 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
2350 git_commit* commit =
nullptr;
2351 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2353 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
2357 git_tree* tree =
nullptr;
2358 git_commit_tree( &tree, commit );
2362 if( aConfirm && aParent )
2364 wxDateTime when( (time_t) git_commit_time( commit ) );
2367 wxString::Format(
_(
"Restore the project to the version from %s?" ),
2368 when.Format( wxS(
"%Y-%m-%d %H:%M:%S" ) ) ),
2369 _(
"Restore Version" ), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION );
2371 dlg.SetYesNoLabels(
_(
"Restore" ),
_(
"Cancel" ) );
2372 dlg.SetExtendedMessage(
_(
"Your current files are backed up first so you can undo the "
2373 "restore. Files that are not part of this version are left "
2376 if( dlg.ShowModal() != wxID_YES )
2378 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: User cancelled at confirm" ) );
2379 git_tree_free( tree );
2380 git_commit_free( commit );
2386 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
2388 std::vector<wxString> backupFiles;
2391 if( !backupFiles.empty() )
2395 backupFiles, wxS(
"Pre-restore backup" ) );
2400 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
2401 git_tree_free( tree );
2402 git_commit_free( commit );
2408 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Current state already matches HEAD; "
2409 "continuing without a new backup commit" ) );
2414 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
2416 if( wxDirExists( tempRestorePath ) )
2417 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2419 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
2422 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
2424 git_tree_free( tree );
2425 git_commit_free( commit );
2429 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
2432 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
2434 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
2435 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2436 git_tree_free( tree );
2437 git_commit_free( commit );
2446 wxString backupPath =
2447 aProjectPath + wxS(
"_restore_backup_" )
2448 + wxDateTime::UNow().Format( wxS(
"%Y-%m-%dT%H-%M-%S-%l" ) );
2450 if( !overlaySnapshotFiles( tempRestorePath, aProjectPath, backupPath ) )
2452 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Overlay failed, rolling back from backup" ) );
2455 if( wxDirExists( backupPath ) )
2457 wxString discard = aProjectPath + wxS(
"_restore_discard" );
2458 overlaySnapshotFiles( backupPath, aProjectPath, discard );
2459 wxFileName::Rmdir( discard, wxPATH_RMDIR_RECURSIVE );
2460 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
2463 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2464 git_tree_free( tree );
2465 git_commit_free( commit );
2471 wxS(
"[history] RestoreCommit: Restore successful, backup retained at %s" ),
2473 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2476 std::vector<wxString> resultFiles;
2479 wxString::Format( wxS(
"Restored from %s" ), aHash ) );
2484 git_tree_free( tree );
2485 git_commit_free( commit );
2487 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
2496 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots =
LoadSnapshots( aProjectPath );
2498 if( snapshots.empty() )
2507 if( !selectedHash.IsEmpty() )
2514 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2517 git_repository* repo =
nullptr;
2519 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2522 git_revwalk* walk =
nullptr;
2523 if( git_revwalk_new( &walk, repo ) != 0 )
2525 git_repository_free( repo );
2529 git_revwalk_sorting( walk, GIT_SORT_TIME );
2530 git_revwalk_push_head( walk );
2534 while( git_revwalk_next( &oid, walk ) == 0 )
2536 git_commit* commit =
nullptr;
2538 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2542 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2543 info.date = wxDateTime(
static_cast<time_t
>( git_commit_time( commit ) ) );
2544 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2546 wxString firstLine =
info.message.BeforeFirst(
'\n' );
2548 long parsedCount = 0;
2550 firstLine.BeforeFirst(
':', &remainder );
2551 remainder.Trim(
true ).Trim(
false );
2553 if( remainder.EndsWith( wxS(
"files changed" ) ) )
2555 wxString countText = remainder.BeforeFirst(
' ' );
2557 if( countText.ToLong( &parsedCount ) )
2558 info.filesChanged =
static_cast<int>( parsedCount );
2561 info.summary = firstLine.BeforeFirst(
':' );
2564 info.message.BeforeFirst(
'\n', &rest );
2565 wxArrayString lines = wxSplit( rest,
'\n',
'\0' );
2567 for(
const wxString& line : lines )
2569 if( !line.IsEmpty() )
2570 info.changedFiles.Add( line );
2573 snapshots.push_back( std::move(
info ) );
2574 git_commit_free( commit );
2577 git_revwalk_free( walk );
2578 git_repository_free( repo );
2590 const wxString& aExtension )
2593 git_repository* repo =
nullptr;
2595 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2596 return wxEmptyString;
2599 git_commit* commit =
nullptr;
2600 git_tree* tree =
nullptr;
2602 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2604 git_repository_free( repo );
2605 return wxEmptyString;
2608 if( git_commit_tree( &tree, commit ) != 0 )
2610 git_commit_free( commit );
2611 git_repository_free( repo );
2612 return wxEmptyString;
2618 std::vector<wxString> entries;
2619 } ctx{ aExtension, {} };
2621 auto collect = [](
const char* aRoot,
const git_tree_entry* aEntry,
void* aPayload ) ->
int
2623 WALK_CTX* c =
static_cast<WALK_CTX*
>( aPayload );
2625 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2628 wxString
name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2630 if( !
name.EndsWith( c->ext ) )
2633 wxString
path = wxString::FromUTF8( aRoot ) +
name;
2634 c->entries.push_back(
path + wxS(
":" )
2635 + wxString::FromUTF8( git_oid_tostr_s( git_tree_entry_id( aEntry ) ) ) );
2639 git_tree_walk( tree, GIT_TREEWALK_PRE, collect, &ctx );
2641 git_tree_free( tree );
2642 git_commit_free( commit );
2643 git_repository_free( repo );
2645 std::sort( ctx.entries.begin(), ctx.entries.end() );
2647 wxString fingerprint;
2649 for(
const wxString& entry : ctx.entries )
2650 fingerprint << entry << wxS(
"|" );
2657 const wxString& aDestDir,
const std::vector<wxString>& aExtensions )
2660 git_repository* repo =
nullptr;
2662 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2666 git_commit* commit =
nullptr;
2667 git_tree* tree =
nullptr;
2669 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2671 git_repository_free( repo );
2675 if( git_commit_tree( &tree, commit ) != 0 )
2677 git_commit_free( commit );
2678 git_repository_free( repo );
2684 git_repository* repo;
2686 const std::vector<wxString>* extensions;
2688 } ctx{ repo, aDestDir, &aExtensions,
true };
2691 auto writeEntry = [](
const char* aRoot,
const git_tree_entry* aEntry,
void* aPayload ) ->
int
2693 WALK_CTX* c =
static_cast<WALK_CTX*
>( aPayload );
2695 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2698 wxString
name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2700 if( !c->extensions->empty() )
2704 for(
const wxString& ext : *c->extensions )
2706 if(
name.EndsWith( ext ) )
2717 wxString rel = wxString::FromUTF8( aRoot ) +
name;
2718 wxFileName outFn( c->destDir + wxS(
"/" ) + rel );
2720 if( !wxFileName::Mkdir( outFn.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
2726 git_blob* blob =
nullptr;
2728 if( git_blob_lookup( &blob, c->repo, git_tree_entry_id( aEntry ) ) == 0 )
2730 const void* data = git_blob_rawcontent( blob );
2731 const size_t size =
static_cast<size_t>( git_blob_rawsize( blob ) );
2732 wxFFile out( outFn.GetFullPath(), wxS(
"wb" ) );
2734 if( !( data && out.IsOpened() && out.Write( data, size ) == size ) )
2737 git_blob_free( blob );
2747 git_tree_walk( tree, GIT_TREEWALK_PRE, writeEntry, &ctx );
2749 git_tree_free( tree );
2750 git_commit_free( commit );
2751 git_repository_free( repo );
bool AutosaveUsesLocalHistory() const
The backup format is the single switch that selects the autosave mechanism: the incremental format re...
wxString GetSelectedHash() const
bool ShouldDescend(const wxString &aDir)
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.
wxString AsString() const
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.
static std::vector< std::pair< wxString, wxString > > CollectAutosaveFilePairs(const wxString &aAutosaveRoot, const wxString &aProjectPath, BACKUP_LOCATION aLocation)
Enumerate every (autosave, source) pair found under aAutosaveRoot for the project at aProjectPath,...
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 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.
std::map< const void *, SAVER_ENTRY > m_savers
bool HeadNewerThanLastSave(const wxString &aProjectPath)
Return true if the autosave data is newer than the last manual save.
std::set< wxString > m_pendingFiles
bool CommitDuplicateOfLastSave(const wxString &aProjectPath, const wxString &aFileType, const wxString &aMessage)
Create a new commit duplicating the tree pointed to by Last_Save_<fileType> and move the Last_Save_<f...
void WaitForPendingSave()
Block until any pending background save completes.
bool RestoreCommit(const wxString &aProjectPath, const wxString &aHash, wxWindow *aParent=nullptr, bool aConfirm=true)
Restore the project files to the state recorded by the given commit hash.
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::vector< LOCAL_HISTORY_SNAPSHOT_INFO > GetSnapshots(const wxString &aProjectPath)
Snapshots (commits) for the project, newest first.
wxString TreeFingerprint(const wxString &aProjectPath, const wxString &aHash, const wxString &aExtension)
Fingerprint of all files ending in aExtension recorded by commit aHash (sorted path:blob pairs).
bool ExtractAllFilesAtCommit(const wxString &aProjectPath, const wxString &aHash, const wxString &aDestDir, const std::vector< wxString > &aExtensions={})
Write files recorded at aHash into aDestDir, recreating the project's relative folder structure.
std::future< bool > m_pendingFuture
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> &aSaver, const std::weak_ptr< void > &aLifetime={})
Register a saver callback invoked during autosave history commits.
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
void pruneExpiredSavers()
Drop tracked savers whose owning document has been freed, before any saver runs.
void RemoveAutosaveFiles(const wxString &aProjectPath) const
Remove every autosave file under the project at aProjectPath regardless of which source it shadowed.
Advisory lock over a file, taken by writing a sibling lock file and holding an exclusive lock on it f...
static LOCKFILE Inspect(const wxString &aFilename)
Look at a lock without taking it: nothing is created, nothing is claimed and nothing is removed on re...
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.
@ 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 bool tagSaveAtHead(git_repository *repo, const wxString &aFileType)
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 localHistoryEnabled()
static bool filesContentEqual(const wxString &aPathA, const wxString &aPathB)
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.
Data produced by a registered saver on the UI thread, consumed by either the background local-history...
std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> saver
std::weak_ptr< void > lifetime
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.