39#include <wx/filename.h>
43#include <wx/datetime.h>
67 const wxString& aRelativePath )
69 wxFileName fn( aRelativePath );
72 return fn.GetFullPath();
77 wxArrayString dirs = fn.GetDirs();
80 dst.AssignDir( aHistoryRoot );
82 for(
const wxString& d : dirs )
85 dst.SetFullName( fn.GetFullName() );
86 return dst.GetFullPath();
96 wxFFile fileA( aPathA, wxS(
"rb" ) );
97 wxFFile fileB( aPathB, wxS(
"rb" ) );
99 if( !fileA.IsOpened() || !fileB.IsOpened() )
102 wxFileOffset lenA = fileA.Length();
103 wxFileOffset lenB = fileB.Length();
105 if( lenA < 0 || lenB < 0 || lenA != lenB )
108 constexpr size_t chunkSize = 64 * 1024;
109 std::vector<char> bufA( chunkSize );
110 std::vector<char> bufB( chunkSize );
112 while( !fileA.Eof() )
114 size_t readA = fileA.Read( bufA.data(), chunkSize );
115 size_t readB = fileB.Read( bufB.data(), chunkSize );
120 if( readA > 0 && std::memcmp( bufA.data(), bufB.data(), readA ) != 0 )
123 if( fileA.Error() || fileB.Error() )
137 const wxString& aRelativePath,
140 wxFileName rel( aRelativePath );
142 dst.AssignDir( aAutosaveRoot );
144 for(
const wxString& d : rel.GetDirs() )
150 dst.SetFullName( rel.GetFullName() );
152 return dst.GetFullPath();
160 const wxString& aProjectPath,
161 const wxString& aAutosaveRoot,
164 wxFileName autosave( aAutosavePath );
168 wxString
name = autosave.GetFullName();
171 return wxEmptyString;
174 return autosave.GetFullPath();
177 if( !aAutosavePath.StartsWith( aAutosaveRoot ) )
178 return wxEmptyString;
180 wxString rel = aAutosavePath.Mid( aAutosaveRoot.length() );
181 wxFileName projFn( aProjectPath, wxEmptyString );
183 return projFn.GetPathWithSep() + rel;
188 const wxString& aTitle );
209 if( aProjectPath.IsEmpty() || !wxDirExists( aProjectPath ) )
212 wxDir dir( aProjectPath );
215 return dir.IsOpened()
226 return aName == wxS(
".history" ) || aName == wxS(
".git" ) || aName == wxS(
"_restore_backup" )
227 || aName.StartsWith( wxS(
"_restore_backup_" ) ) || aName == wxS(
"_restore_temp" )
242 wxFileName fn( aFile );
252 const void* aSaverObject,
253 const std::function<
void(
const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver,
254 const std::weak_ptr<void>& aLifetime )
258 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping" ), aSaverObject );
263 entry.
saver = aSaver;
268 entry.
tracked = aLifetime.lock() !=
nullptr;
270 m_savers[aSaverObject] = std::move( entry );
271 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)" ), aSaverObject,
m_savers.size() );
277 std::vector<const void*> expired;
281 for(
const auto& [saverObject, entry] :
m_savers )
283 if( entry.tracked && entry.lifetime.expired() )
284 expired.push_back( saverObject );
287 for(
const void* obj : expired )
296 auto it =
m_savers.find( aSaverObject );
301 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)" ),
311 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers" ) );
316 const wxString& aTagFileType )
320 wxLogTrace(
traceAutoSave, wxS(
"Local history disabled, returning" ) );
327 Init( aProjectPath );
330 wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu tag='%s'" ),
331 aProjectPath, aTitle,
m_savers.size(), aTagFileType );
335 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
340 if( !aTagFileType.IsEmpty() )
346 wxLogTrace(
traceAutoSave, wxS(
"[history] previous save still in progress; skipping cycle" ) );
353 std::vector<HISTORY_FILE_DATA> fileData;
355 for(
const auto& [saverObject, entry] :
m_savers )
357 size_t before = fileData.size();
358 entry.saver( aProjectPath, fileData );
359 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p produced %zu entries (total=%zu)" ),
360 saverObject, fileData.size() - before, fileData.size() );
366 fileData.erase( std::remove_if( fileData.begin(), fileData.end(),
369 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
371 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out entry with invalid path: '%s'" ),
372 entry.relativePath );
379 if( fileData.empty() )
381 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no entries; skipping" ) );
386 m_saveInProgress.store(
true, std::memory_order_release );
389 [
this, projectPath = aProjectPath, title = aTitle, tagFileType = aTagFileType,
390 data = std::move( fileData )]()
mutable ->
bool
392 bool result = commitInBackground( projectPath, title, data, !tagFileType.IsEmpty() );
394 if( !tagFileType.IsEmpty() )
395 TagSave( projectPath, tagFileType );
397 m_saveInProgress.store(
false, std::memory_order_release );
402 if( !aTagFileType.IsEmpty() )
403 WaitForPendingSave();
411 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
416 wxLogTrace(
traceAutoSave, wxS(
"[autosave] no savers registered; skipping" ) );
426 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create autosave root '%s'" ), autosaveRoot );
432 std::vector<HISTORY_FILE_DATA> fileData;
434 for(
const auto& [saverObject, entry] :
m_savers )
435 entry.saver( aProjectPath, fileData );
437 bool anyWritten =
false;
441 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
445 wxFileName dstFn( dst );
449 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create dir '%s'" ), dstFn.GetPath() );
455 if( !entry.content.empty() )
457 buf = std::move( entry.content );
462 else if( !entry.sourcePath.IsEmpty() )
464 wxFFile src( entry.sourcePath, wxS(
"rb" ) );
466 if( !src.IsOpened() )
469 wxFileOffset len = src.Length();
474 buf.resize(
static_cast<size_t>( len ) );
476 if( len > 0 && src.Read( buf.data(), buf.size() ) != buf.size() )
492 wxLogTrace(
traceAutoSave, wxS(
"[autosave] wrote %zu bytes to '%s'" ), buf.size(), dst );
496 wxLogTrace(
traceAutoSave, wxS(
"[autosave] write failed for '%s': %s" ), dst, err );
509std::vector<std::pair<wxString, wxString>>
513 std::vector<std::pair<wxString, wxString>> results;
515 if( !wxDirExists( aAutosaveRoot ) )
523 std::function<void(
const wxString& )> walk =
524 [&](
const wxString& aDir )
532 bool cont = d.GetFirst( &
name );
536 wxFileName fn( aDir,
name );
537 wxString fullPath = fn.GetFullPath();
539 if( wxDirExists( fullPath ) )
542 && (
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) ) )
544 cont = d.GetNext( &
name );
558 results.emplace_back( fullPath, src );
561 cont = d.GetNext( &
name );
565 walk( aAutosaveRoot );
570static std::vector<std::pair<wxString, wxString>>
581std::vector<std::pair<wxString, wxString>>
584 std::vector<std::pair<wxString, wxString>> results;
586 if( aExtensions.empty() )
591 wxFileName srcFn( pair.second );
594 for(
const wxString& ext : aExtensions )
596 if( srcFn.GetExt().IsSameAs( ext,
false ) )
608 if( srcFn.FileExists() )
609 srcTime = srcFn.GetModificationTime();
611 wxDateTime autosaveTime = wxFileName( pair.first ).GetModificationTime();
615 bool stale = !srcTime.IsValid()
616 || ( autosaveTime.IsLaterThan( srcTime )
620 results.emplace_back( std::move( pair ) );
634 if( wxFileExists( autosavePath ) )
635 wxRemoveFile( autosavePath );
641 const std::vector<wxString>& aSourcePaths )
const
643 if( aSourcePaths.empty() )
646 std::vector<wxFileName> targets;
647 targets.reserve( aSourcePaths.size() );
649 for(
const wxString& src : aSourcePaths )
652 targets.emplace_back( src );
655 if( targets.empty() )
660 wxFileName srcFn( srcPath );
663 for(
const wxFileName& target : targets )
665 if( srcFn.SameAs( target ) )
672 if( match && wxFileExists( autosavePath ) )
673 wxRemoveFile( autosavePath );
679 const std::vector<HISTORY_FILE_DATA>& aFileData,
bool aIsManualSave )
681 wxLogTrace(
traceAutoSave, wxS(
"[history] background: writing %zu entries for '%s'" ),
682 aFileData.size(), aProjectPath );
688 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create history root '%s'" ), hist );
695 wxFileName dstFn( dst );
696 wxString parent = dstFn.GetPath();
700 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create dir '%s'" ), parent );
704 if( !entry.content.empty() )
706 std::string buf = entry.content;
711 wxFFile fp( dst, wxS(
"wb" ) );
715 fp.Write( buf.data(), buf.size() );
717 wxLogTrace(
traceAutoSave, wxS(
"[history] background: wrote %zu bytes to '%s'" ), buf.size(), dst );
721 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to open '%s' for writing" ), dst );
724 else if( !entry.sourcePath.IsEmpty() )
726 wxCopyFile( entry.sourcePath, dst,
true );
727 wxLogTrace(
traceAutoSave, wxS(
"[history] background: copied '%s' -> '%s'" ), entry.sourcePath, dst );
743 git_repository_set_workdir( repo, hist.mb_str().data(),
false );
749 wxString rel = entry.relativePath;
750 rel.Replace( wxS(
"\\" ), wxS(
"/" ) );
754 if( !wxFileExists( abs ) )
757 git_index_add_bypath(
index, rel.ToStdString().c_str() );
762 git_commit* head_commit =
nullptr;
763 git_tree* head_tree =
nullptr;
765 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
766 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
767 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
769 git_tree* rawIndexTree =
nullptr;
770 git_oid index_tree_oid;
772 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
775 git_tree_free( head_tree );
778 git_commit_free( head_commit );
780 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to write index tree" ) );
784 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
785 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
787 bool hasChanges =
true;
791 git_diff* diff =
nullptr;
793 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
795 hasChanges = git_diff_num_deltas( diff ) > 0;
796 wxLogTrace(
traceAutoSave, wxS(
"[history] background: diff deltas=%u" ),
797 (
unsigned) git_diff_num_deltas( diff ) );
798 git_diff_free( diff );
805 bool stagedMatchesDisk =
true;
809 wxString diskPath = aProjectPath + wxFileName::GetPathSeparator() + entry.relativePath;
812 if( !wxFileExists( diskPath ) || !wxFileExists( histPath ) )
814 stagedMatchesDisk =
false;
818 wxFFile diskFile( diskPath, wxT(
"rb" ) );
819 wxFFile histFile( histPath, wxT(
"rb" ) );
821 if( !diskFile.IsOpened() || !histFile.IsOpened() || diskFile.Length() != histFile.Length() )
823 stagedMatchesDisk =
false;
827 size_t len =
static_cast<size_t>( diskFile.Length() );
828 std::string diskBuf( len,
'\0' );
829 std::string histBuf( len,
'\0' );
831 if( diskFile.Read( diskBuf.data(), len ) != len
832 || histFile.Read( histBuf.data(), len ) != len
833 || diskBuf != histBuf )
835 stagedMatchesDisk =
false;
840 if( stagedMatchesDisk && !aIsManualSave )
842 wxLogTrace(
traceAutoSave, wxS(
"[history] background: first commit; staged matches disk -- skipping" ) );
848 git_tree_free( head_tree );
851 git_commit_free( head_commit );
855 wxLogTrace(
traceAutoSave, wxS(
"[history] background: no changes detected; no commit") );
859 if( !aTitle.IsEmpty() && aTitle != wxS(
"Autosave" ) )
861 git_oid head_oid_amend;
863 if( git_reference_name_to_id( &head_oid_amend, repo,
"HEAD" ) == 0 )
865 git_commit* head_commit_amend =
nullptr;
867 if( git_commit_lookup( &head_commit_amend, repo, &head_oid_amend ) == 0 )
869 wxString existingMsg = wxString::FromUTF8( git_commit_message( head_commit_amend ) );
870 existingMsg.Trim(
true ).Trim(
false );
872 if( existingMsg != aTitle )
875 int amend_rc = git_commit_amend( &amended_oid, head_commit_amend,
"HEAD",
nullptr,
nullptr,
876 nullptr, aTitle.mb_str().data(),
nullptr );
879 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amended HEAD message '%s' -> '%s'" ),
880 existingMsg, aTitle );
882 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amend failed rc=%d" ), amend_rc );
885 git_commit_free( head_commit_amend );
893 git_signature* rawSig =
nullptr;
895 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
897 git_commit* parent =
nullptr;
901 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
903 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
907 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
909 const git_commit* constParent = parent;
911 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
912 msg.mb_str().data(), indexTree.get(), parents,
913 parents ? &constParent :
nullptr );
917 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit created %s (%s entries=%zu)" ),
918 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
922 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit failed rc=%d" ), rc );
926 git_commit_free( parent );
928 git_index_write(
index );
937 wxLogTrace(
traceAutoSave, wxS(
"[history] waiting for pending background save" ) );
961 if( !wxDirExists( hist ) )
963 wxLogNull suppressSysErrorPopups;
972 git_repository* rawRepo =
nullptr;
974 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
976 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
979 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
980 if( !ignoreFile.FileExists() )
982 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
985 f.Write( wxS(
"# KiCad local history exclusions. Edit to add your own rules.\n"
992 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
994 if( !readmeFile.FileExists() )
996 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
1000 f.Write( wxS(
"KiCad Local History Directory\n"
1001 "=============================\n\n"
1002 "This directory contains automatic snapshots of your project files.\n"
1003 "KiCad periodically saves copies of your work here, allowing you to\n"
1004 "recover from accidental changes or data loss.\n\n"
1005 "You can browse and restore previous versions through KiCad's\n"
1006 "File > Local History menu.\n\n"
1007 "To disable this feature:\n"
1008 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
1009 "This directory can be safely deleted if you no longer need the\n"
1010 "history, but doing so will permanently remove all saved snapshots.\n" ) );
1016 git_repository_free( rawRepo );
1032 const wxString& aHistoryPath,
const wxString& aProjectPath,
1033 const std::vector<wxString>& aFiles,
const wxString& aTitle )
1035 std::vector<std::string> filesArrStr;
1037 for(
const wxString& file : aFiles )
1039 wxFileName src( file );
1042 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
1043 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
1045 relPath = src.GetFullName();
1047 relPath.Replace(
"\\",
"/" );
1048 std::string relPathStr = relPath.ToStdString();
1050 unsigned int status = 0;
1051 int rc = git_status_file( &status, repo, relPathStr.data() );
1053 if( rc == 0 && status != 0 )
1055 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
1056 filesArrStr.emplace_back( relPathStr );
1060 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
1061 filesArrStr.emplace_back( relPathStr );
1065 std::vector<char*> cStrings( filesArrStr.size() );
1067 for(
size_t i = 0; i < filesArrStr.size(); i++ )
1068 cStrings[i] = filesArrStr[i].data();
1070 git_strarray filesArrGit;
1071 filesArrGit.count = filesArrStr.size();
1072 filesArrGit.strings = cStrings.data();
1074 if( filesArrStr.size() == 0 )
1080 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
1082 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
1088 if( git_index_write_tree( &tree_id,
index ) != 0 )
1091 git_tree* rawTree =
nullptr;
1092 git_tree_lookup( &rawTree, repo, &tree_id );
1093 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
1095 git_signature* rawSig =
nullptr;
1097 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
1098 &git_signature_free );
1100 git_commit* rawParent =
nullptr;
1104 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
1106 git_commit_lookup( &rawParent, repo, &parent_id );
1110 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
1113 git_tree* rawParentTree =
nullptr;
1116 git_commit_tree( &rawParentTree, parent.get() );
1118 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
1120 git_diff* rawDiff =
nullptr;
1121 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
1122 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
1124 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
1126 if( numChangedFiles == 0 )
1128 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
1134 if( !aTitle.IsEmpty() )
1135 msg << aTitle << wxS(
": " );
1137 msg << numChangedFiles << wxS(
" files changed" );
1139 for(
size_t i = 0; i < numChangedFiles; ++i )
1141 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
1142 git_patch* rawPatch =
nullptr;
1143 git_patch_from_diff( &rawPatch, diff.get(), i );
1144 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
1146 size_t context = 0, adds = 0, dels = 0;
1147 git_patch_line_stats( &context, &adds, &dels, patch.get() );
1148 size_t updated = std::min( adds, dels );
1151 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
1152 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
1156 git_commit* parentPtr = parent.get();
1157 const git_commit* constParentPtr = parentPtr;
1158 if( git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr, msg.mb_str().data(), tree.get(),
1159 parents, parentPtr ? &constParentPtr :
nullptr )
1165 git_index_write(
index );
1174 const wxString& aTitle )
1182 wxLogTrace(
traceAutoSave, wxS(
"[history] commitSnapshotForProject failed to acquire lock: %s" ),
1199 wxString proj = wxFileName( aFiles[0] ).GetPath();
1213 wxString
name = aFile.GetFullName();
1215 if(
name == wxS(
"sym-lib-table" ) ||
name == wxS(
"fp-lib-table" ) )
1218 return aFile.GetExt().StartsWith( wxS(
"kicad_" ) );
1227 wxDir dir( aProjectPath );
1229 if( !dir.IsOpened() )
1240 std::function<void(
const wxString&,
bool )> collect =
1241 [&](
const wxString&
path,
bool topLevel )
1246 wxS(
"[history] collectProjectFiles: Skipping nested project at %s" ),
1257 bool cont = d.GetFirst( &
name );
1263 cont = d.GetNext( &
name );
1268 wxString fullPath = fn.GetFullPath();
1270 if( wxFileName::DirExists( fullPath ) )
1273 collect( fullPath,
false );
1275 else if( fn.FileExists() && fn.GetFullName() != wxS(
"fp-info-cache" ) &&
isKiCadProjectFile( fn ) )
1277 aFiles.push_back( fn.GetFullPath() );
1280 cont = d.GetNext( &
name );
1284 collect( aProjectPath,
true );
1293 std::vector<wxString> files;
1299 Init( aProjectPath );
1305 return wxDirExists(
historyPath( aProjectPath ) );
1317 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
1322 git_reference* ref =
nullptr;
1325 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
1326 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
1329 git_object* head_obj =
nullptr;
1330 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
1331 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
1332 git_object_free( head_obj );
1335 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
1336 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
1338 git_reference_delete( ref );
1339 git_reference_free( ref );
1342 git_oid last_tag_oid;
1343 git_object* head_obj2 =
nullptr;
1344 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
1345 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
1346 git_object_free( head_obj2 );
1364 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
1374 git_repository* repo =
nullptr;
1376 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1380 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1382 git_repository_free( repo );
1386 git_commit* head_commit =
nullptr;
1387 git_commit_lookup( &head_commit, repo, &head_oid );
1388 git_time_t head_time = git_commit_time( head_commit );
1391 git_tag_list_match( &tags,
"Last_Save_*", repo );
1392 git_time_t save_time = 0;
1394 for(
size_t i = 0; i < tags.count; ++i )
1396 git_reference* ref =
nullptr;
1397 if( git_reference_lookup( &ref, repo,
1398 ( wxS(
"refs/tags/" ) +
1399 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
1401 const git_oid* oid = git_reference_target( ref );
1402 git_commit* c =
nullptr;
1403 if( git_commit_lookup( &c, repo, oid ) == 0 )
1405 git_time_t t = git_commit_time( c );
1408 git_commit_free( c );
1410 git_reference_free( ref );
1414 git_strarray_free( &tags );
1415 git_commit_free( head_commit );
1416 git_repository_free( repo );
1420 if( save_time == 0 )
1423 return head_time > save_time;
1427 const wxString& aMessage )
1439 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
1448 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
1449 git_reference* lastRef =
nullptr;
1450 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
1452 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
1454 const git_oid* lastOid = git_reference_target( lastRef );
1455 git_commit* lastCommit =
nullptr;
1456 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
1458 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
1460 git_tree* lastTree =
nullptr;
1461 git_commit_tree( &lastTree, lastCommit );
1462 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
1466 git_commit* headCommit =
nullptr;
1468 const git_commit* parentArray[1];
1469 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
1470 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
1472 parentArray[0] = headCommit;
1476 git_signature* sigRaw =
nullptr;
1478 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
1480 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
1481 git_oid newCommitOid;
1482 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
1483 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
1484 if( headCommit ) git_commit_free( headCommit );
1489 git_reference* existing =
nullptr;
1490 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
1492 git_reference_delete( existing );
1493 git_reference_free( existing );
1495 git_object* newCommitObj =
nullptr;
1496 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
1498 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
1499 git_object_free( newCommitObj );
1508 if( !dir.IsOpened() )
1511 bool cont = dir.GetFirst( &
name );
1515 wxString fullPath = fn.GetFullPath();
1517 if( wxFileName::DirExists( fullPath ) )
1519 else if( fn.FileExists() )
1520 total += (size_t) fn.GetSize().GetValue();
1521 cont = dir.GetNext( &
name );
1527static bool copyTreeObjects( git_repository* aSrcRepo, git_odb* aSrcOdb, git_odb* aDstOdb,
const git_oid* aTreeOid,
1528 std::set<git_oid,
bool ( * )(
const git_oid&,
const git_oid& )>& aCopied )
1530 if( aCopied.count( *aTreeOid ) )
1533 git_odb_object* obj =
nullptr;
1535 if( git_odb_read( &obj, aSrcOdb, aTreeOid ) != 0 )
1539 int err = git_odb_write( &written, aDstOdb, git_odb_object_data( obj ), git_odb_object_size( obj ),
1540 git_odb_object_type( obj ) );
1541 git_odb_object_free( obj );
1546 aCopied.insert( *aTreeOid );
1548 git_tree* tree =
nullptr;
1550 if( git_tree_lookup( &tree, aSrcRepo, aTreeOid ) != 0 )
1553 size_t cnt = git_tree_entrycount( tree );
1555 for(
size_t i = 0; i < cnt; ++i )
1557 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1558 const git_oid* entryId = git_tree_entry_id( entry );
1560 if( aCopied.count( *entryId ) )
1563 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1565 if( !
copyTreeObjects( aSrcRepo, aSrcOdb, aDstOdb, entryId, aCopied ) )
1567 git_tree_free( tree );
1571 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1573 git_odb_object* blobObj =
nullptr;
1575 if( git_odb_read( &blobObj, aSrcOdb, entryId ) == 0 )
1577 git_oid blobWritten;
1579 if( git_odb_write( &blobWritten, aDstOdb, git_odb_object_data( blobObj ),
1580 git_odb_object_size( blobObj ), git_odb_object_type( blobObj ) )
1583 git_odb_object_free( blobObj );
1584 git_tree_free( tree );
1588 git_odb_object_free( blobObj );
1589 aCopied.insert( *entryId );
1594 git_tree_free( tree );
1603 git_packbuilder* pb =
nullptr;
1605 if( git_packbuilder_new( &pb, aRepo ) != 0 )
1608 git_revwalk* walk =
nullptr;
1610 if( git_revwalk_new( &walk, aRepo ) != 0 )
1612 git_packbuilder_free( pb );
1616 git_revwalk_push_head( walk );
1619 while( git_revwalk_next( &oid, walk ) == 0 )
1621 if( git_packbuilder_insert_commit( pb, &oid ) != 0 )
1623 git_revwalk_free( walk );
1624 git_packbuilder_free( pb );
1629 git_revwalk_free( walk );
1633 git_packbuilder_set_callbacks(
1635 [](
int aStage, uint32_t aCurrent, uint32_t aTotal,
void* aPayload )
1640 reporter->SetCurrentProgress( (
double) aCurrent / aTotal );
1648 if( git_packbuilder_write( pb,
nullptr, 0,
nullptr,
nullptr ) != 0 )
1650 git_packbuilder_free( pb );
1654 git_packbuilder_free( pb );
1656 wxString objPath = wxString::FromUTF8( git_repository_path( aRepo ) ) + wxS(
"objects" );
1657 wxDir objDir( objPath );
1659 if( objDir.IsOpened() )
1661 wxArrayString toRemove;
1663 bool cont = objDir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS );
1667 if(
name.length() == 2 )
1668 toRemove.Add( objPath + wxFileName::GetPathSeparator() +
name );
1670 cont = objDir.GetNext( &
name );
1673 for(
const wxString& dir : toRemove )
1674 wxFileName::Rmdir( dir, wxPATH_RMDIR_RECURSIVE );
1683 if( aMaxBytes == 0 )
1688 if( !wxDirExists( hist ) )
1693 if( current <= aMaxBytes )
1700 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
1710 aReporter->
Report(
_(
"Compacting local history..." ) );
1717 if( current <= aMaxBytes )
1721 git_revwalk* walk =
nullptr;
1722 git_revwalk_new( &walk, repo );
1723 git_revwalk_sorting( walk, GIT_SORT_TIME );
1724 git_revwalk_push_head( walk );
1725 std::vector<git_oid> commits;
1728 while( git_revwalk_next( &oid, walk ) == 0 )
1729 commits.push_back( oid );
1731 git_revwalk_free( walk );
1733 if( commits.empty() )
1737 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
1738 [](
const git_oid& a,
const git_oid& b )
1740 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1743 size_t keptBytes = 0;
1744 std::vector<git_oid> keep;
1746 git_odb* odb =
nullptr;
1747 git_repository_odb( &odb, repo );
1749 std::function<size_t( git_tree* )> accountTree =
1750 [&]( git_tree* tree )
1753 size_t cnt = git_tree_entrycount( tree );
1755 for(
size_t i = 0; i < cnt; ++i )
1757 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1759 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1761 const git_oid* bid = git_tree_entry_id( entry );
1763 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1766 git_object_t type = GIT_OBJECT_ANY;
1768 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1771 seenBlobs.insert( *bid );
1774 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1776 git_tree* sub =
nullptr;
1778 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1780 added += accountTree( sub );
1781 git_tree_free( sub );
1789 for(
const git_oid& cOid : commits )
1791 git_commit* c =
nullptr;
1793 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1796 git_tree* tree =
nullptr;
1797 git_commit_tree( &tree, c );
1798 size_t add = accountTree( tree );
1799 git_tree_free( tree );
1800 git_commit_free( c );
1802 if( keep.empty() || keptBytes + add <= aMaxBytes )
1804 keep.push_back( cOid );
1812 keep.push_back( commits.front() );
1816 std::vector<std::pair<wxString, git_oid>> tagTargets;
1817 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
1818 [](
const git_oid& a,
const git_oid& b )
1820 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1822 git_strarray tagList;
1824 if( git_tag_list( &tagList, repo ) == 0 )
1826 for(
size_t i = 0; i < tagList.count; ++i )
1828 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1829 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1831 git_reference* tref =
nullptr;
1833 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1835 const git_oid* toid = git_reference_target( tref );
1839 tagTargets.emplace_back(
name, *toid );
1840 taggedCommits.insert( *toid );
1844 for(
const auto& k : keep )
1846 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1856 keep.push_back( *toid );
1857 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1862 git_reference_free( tref );
1866 git_strarray_free( &tagList );
1870 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1871 wxString trimPath = trimFn.GetPath();
1873 if( wxDirExists( trimPath ) )
1874 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1876 wxMkdir( trimPath );
1877 git_repository* newRepo =
nullptr;
1879 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1881 git_odb_free( odb );
1885 git_odb* dstOdb =
nullptr;
1887 if( git_repository_odb( &dstOdb, newRepo ) != 0 )
1889 git_repository_free( newRepo );
1890 git_odb_free( odb );
1894 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> copiedObjects(
1895 [](
const git_oid& a,
const git_oid& b )
1897 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1901 std::reverse( keep.begin(), keep.end() );
1902 git_commit* parent =
nullptr;
1903 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1904 std::vector<MAP_ENTRY> commitMap;
1908 aReporter->
AdvancePhase(
_(
"Trimming local history..." ) );
1912 for(
size_t idx = 0; idx < keep.size(); ++idx )
1917 const git_oid& co = keep[idx];
1918 git_commit* orig =
nullptr;
1920 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1923 git_tree* tree =
nullptr;
1924 git_commit_tree( &tree, orig );
1926 copyTreeObjects( repo, odb, dstOdb, git_tree_id( tree ), copiedObjects );
1928 git_tree* newTree =
nullptr;
1929 git_tree_lookup( &newTree, newRepo, git_tree_id( tree ) );
1931 git_tree_free( tree );
1934 const git_signature* origAuthor = git_commit_author( orig );
1935 const git_signature* origCommitter = git_commit_committer( orig );
1936 git_signature* sigAuthor =
nullptr;
1937 git_signature* sigCommitter =
nullptr;
1939 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1940 origAuthor->when.time, origAuthor->when.offset );
1941 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1942 origCommitter->when.time, origCommitter->when.offset );
1944 const git_commit* parents[1];
1945 int parentCount = 0;
1949 parents[0] = parent;
1953 git_oid newCommitOid;
1954 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1955 newTree, parentCount, parentCount ? parents :
nullptr );
1958 git_commit_free( parent );
1960 git_commit_lookup( &parent, newRepo, &newCommitOid );
1962 commitMap.emplace_back( co, newCommitOid );
1964 git_signature_free( sigAuthor );
1965 git_signature_free( sigCommitter );
1966 git_tree_free( newTree );
1967 git_commit_free( orig );
1971 git_commit_free( parent );
1974 for(
const auto& tt : tagTargets )
1977 const git_oid* newOid =
nullptr;
1979 for(
const auto& m : commitMap )
1981 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1991 git_object* obj =
nullptr;
1993 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1995 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1996 git_object_free( obj );
2001 aReporter->
AdvancePhase(
_(
"Compacting trimmed history..." ) );
2008 git_odb_free( dstOdb );
2009 git_odb_free( odb );
2010 git_repository_free( newRepo );
2015 wxString backupOld = hist + wxS(
"_old");
2016 wxRenameFile( hist, backupOld );
2017 wxRenameFile( trimPath, hist );
2018 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
2025 git_repository* repo =
nullptr;
2027 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2028 return wxEmptyString;
2031 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
2033 git_repository_free( repo );
2034 return wxEmptyString;
2037 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
2038 git_repository_free( repo );
2050bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
2052 std::function<void(
const wxString& )> findLocks =
2053 [&](
const wxString& dirPath )
2055 wxDir dir( dirPath );
2056 if( !dir.IsOpened() )
2060 bool cont = dir.GetFirst( &filename );
2064 wxFileName fullPath( dirPath, filename );
2067 if( filename == wxS(
".history") || filename == wxS(
".git") )
2069 cont = dir.GetNext( &filename );
2073 if( fullPath.DirExists() )
2075 findLocks( fullPath.GetFullPath() );
2077 else if( fullPath.FileExists()
2084 baseName = baseName.BeforeLast(
'.' );
2085 wxFileName originalFile( dirPath, baseName );
2088 LOCKFILE testLock( originalFile.GetFullPath() );
2089 if( testLock.Valid() && !testLock.IsLockedByMe() )
2091 aLockedFiles.push_back( fullPath.GetFullPath() );
2095 cont = dir.GetNext( &filename );
2099 findLocks( aProjectPath );
2100 return aLockedFiles.empty();
2107bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
2109 bool extractSuccess =
true;
2111 std::function<void( git_tree*,
const wxString& )> extractTree =
2112 [&]( git_tree* t,
const wxString& prefix )
2114 if( !extractSuccess )
2117 size_t cnt = git_tree_entrycount( t );
2118 for(
size_t i = 0; i < cnt; ++i )
2120 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
2121 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
2122 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
2124 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
2126 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath, wxEmptyString );
2128 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
2131 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
2132 dirPath.GetPath() );
2133 extractSuccess =
false;
2137 git_tree* sub =
nullptr;
2139 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
2141 extractTree( sub, fullPath );
2142 git_tree_free( sub );
2145 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
2147 git_blob* blob =
nullptr;
2149 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
2151 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
2153 wxFileName dstDir( dst );
2154 dstDir.SetFullName( wxEmptyString );
2155 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
2157 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
2161 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
2167 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
2168 dst.GetFullPath() );
2169 extractSuccess =
false;
2170 git_blob_free( blob );
2174 git_blob_free( blob );
2180 extractTree( aTree, wxEmptyString );
2181 return extractSuccess;
2191void collectRelativeFiles(
const wxString& aRoot,
const wxString& aDir, std::vector<wxString>& aOut )
2195 if( !dir.IsOpened() )
2200 for(
bool cont = dir.GetFirst( &
name ); cont; cont = dir.GetNext( &
name ) )
2202 wxString full = aDir + wxFILE_SEP_PATH +
name;
2204 if( wxDirExists( full ) )
2206 collectRelativeFiles( aRoot, full, aOut );
2208 else if( wxFileExists( full ) )
2210 wxString rel = full.Mid( aRoot.length() + 1 );
2211 rel.Replace( wxS(
"\\" ), wxS(
"/" ) );
2212 aOut.push_back( rel );
2226bool overlaySnapshotFiles(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
const wxString& aBackupPath )
2228 std::vector<wxString> relPaths;
2229 collectRelativeFiles( aTempRestorePath, aTempRestorePath, relPaths );
2231 for(
const wxString& rel : relPaths )
2233 wxFileName src( aTempRestorePath + wxFILE_SEP_PATH + rel );
2234 wxFileName dst( aProjectPath + wxFILE_SEP_PATH + rel );
2236 if( dst.FileExists() )
2238 wxFileName bak( aBackupPath + wxFILE_SEP_PATH + rel );
2240 if( !wxFileName::Mkdir( bak.GetPath(), 0777, wxPATH_MKDIR_FULL )
2241 || !wxCopyFile( dst.GetFullPath(), bak.GetFullPath(),
true ) )
2247 if( !wxFileName::Mkdir( dst.GetPath(), 0777, wxPATH_MKDIR_FULL )
2248 || !wxCopyFile( src.GetFullPath(), dst.GetFullPath(),
true ) )
2265 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
2268 std::vector<wxString> lockedFiles;
2269 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
2272 for(
const auto& f : lockedFiles )
2273 lockList += wxS(
"\n - ") + f;
2276 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
2282 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
2284 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
2295 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
2306 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
2308 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
2312 git_commit* commit =
nullptr;
2313 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2315 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
2319 git_tree* tree =
nullptr;
2320 git_commit_tree( &tree, commit );
2324 if( aConfirm && aParent )
2326 wxDateTime when( (time_t) git_commit_time( commit ) );
2329 wxString::Format(
_(
"Restore the project to the version from %s?" ),
2330 when.Format( wxS(
"%Y-%m-%d %H:%M:%S" ) ) ),
2331 _(
"Restore Version" ), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION );
2333 dlg.SetYesNoLabels(
_(
"Restore" ),
_(
"Cancel" ) );
2334 dlg.SetExtendedMessage(
_(
"Your current files are backed up first so you can undo the "
2335 "restore. Files that are not part of this version are left "
2338 if( dlg.ShowModal() != wxID_YES )
2340 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: User cancelled at confirm" ) );
2341 git_tree_free( tree );
2342 git_commit_free( commit );
2348 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
2350 std::vector<wxString> backupFiles;
2353 if( !backupFiles.empty() )
2357 backupFiles, wxS(
"Pre-restore backup" ) );
2362 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
2363 git_tree_free( tree );
2364 git_commit_free( commit );
2370 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Current state already matches HEAD; "
2371 "continuing without a new backup commit" ) );
2376 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
2378 if( wxDirExists( tempRestorePath ) )
2379 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2381 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
2384 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
2386 git_tree_free( tree );
2387 git_commit_free( commit );
2391 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
2394 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
2396 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
2397 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2398 git_tree_free( tree );
2399 git_commit_free( commit );
2408 wxString backupPath =
2409 aProjectPath + wxS(
"_restore_backup_" )
2410 + wxDateTime::UNow().Format( wxS(
"%Y-%m-%dT%H-%M-%S-%l" ) );
2412 if( !overlaySnapshotFiles( tempRestorePath, aProjectPath, backupPath ) )
2414 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Overlay failed, rolling back from backup" ) );
2417 if( wxDirExists( backupPath ) )
2419 wxString discard = aProjectPath + wxS(
"_restore_discard" );
2420 overlaySnapshotFiles( backupPath, aProjectPath, discard );
2421 wxFileName::Rmdir( discard, wxPATH_RMDIR_RECURSIVE );
2422 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
2425 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2426 git_tree_free( tree );
2427 git_commit_free( commit );
2433 wxS(
"[history] RestoreCommit: Restore successful, backup retained at %s" ),
2435 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2438 std::vector<wxString> resultFiles;
2441 wxString::Format( wxS(
"Restored from %s" ), aHash ) );
2446 git_tree_free( tree );
2447 git_commit_free( commit );
2449 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
2458 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots =
LoadSnapshots( aProjectPath );
2460 if( snapshots.empty() )
2469 if( !selectedHash.IsEmpty() )
2476 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2479 git_repository* repo =
nullptr;
2481 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2484 git_revwalk* walk =
nullptr;
2485 if( git_revwalk_new( &walk, repo ) != 0 )
2487 git_repository_free( repo );
2491 git_revwalk_sorting( walk, GIT_SORT_TIME );
2492 git_revwalk_push_head( walk );
2496 while( git_revwalk_next( &oid, walk ) == 0 )
2498 git_commit* commit =
nullptr;
2500 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2504 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2505 info.date = wxDateTime(
static_cast<time_t
>( git_commit_time( commit ) ) );
2506 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2508 wxString firstLine =
info.message.BeforeFirst(
'\n' );
2510 long parsedCount = 0;
2512 firstLine.BeforeFirst(
':', &remainder );
2513 remainder.Trim(
true ).Trim(
false );
2515 if( remainder.EndsWith( wxS(
"files changed" ) ) )
2517 wxString countText = remainder.BeforeFirst(
' ' );
2519 if( countText.ToLong( &parsedCount ) )
2520 info.filesChanged =
static_cast<int>( parsedCount );
2523 info.summary = firstLine.BeforeFirst(
':' );
2526 info.message.BeforeFirst(
'\n', &rest );
2527 wxArrayString lines = wxSplit( rest,
'\n',
'\0' );
2529 for(
const wxString& line : lines )
2531 if( !line.IsEmpty() )
2532 info.changedFiles.Add( line );
2535 snapshots.push_back( std::move(
info ) );
2536 git_commit_free( commit );
2539 git_revwalk_free( walk );
2540 git_repository_free( repo );
2552 const wxString& aExtension )
2555 git_repository* repo =
nullptr;
2557 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2558 return wxEmptyString;
2561 git_commit* commit =
nullptr;
2562 git_tree* tree =
nullptr;
2564 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2566 git_repository_free( repo );
2567 return wxEmptyString;
2570 if( git_commit_tree( &tree, commit ) != 0 )
2572 git_commit_free( commit );
2573 git_repository_free( repo );
2574 return wxEmptyString;
2580 std::vector<wxString> entries;
2581 } ctx{ aExtension, {} };
2583 auto collect = [](
const char* aRoot,
const git_tree_entry* aEntry,
void* aPayload ) ->
int
2585 WALK_CTX* c =
static_cast<WALK_CTX*
>( aPayload );
2587 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2590 wxString
name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2592 if( !
name.EndsWith( c->ext ) )
2595 wxString
path = wxString::FromUTF8( aRoot ) +
name;
2596 c->entries.push_back(
path + wxS(
":" )
2597 + wxString::FromUTF8( git_oid_tostr_s( git_tree_entry_id( aEntry ) ) ) );
2601 git_tree_walk( tree, GIT_TREEWALK_PRE, collect, &ctx );
2603 git_tree_free( tree );
2604 git_commit_free( commit );
2605 git_repository_free( repo );
2607 std::sort( ctx.entries.begin(), ctx.entries.end() );
2609 wxString fingerprint;
2611 for(
const wxString& entry : ctx.entries )
2612 fingerprint << entry << wxS(
"|" );
2619 const wxString& aDestDir,
const std::vector<wxString>& aExtensions )
2622 git_repository* repo =
nullptr;
2624 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2628 git_commit* commit =
nullptr;
2629 git_tree* tree =
nullptr;
2631 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2633 git_repository_free( repo );
2637 if( git_commit_tree( &tree, commit ) != 0 )
2639 git_commit_free( commit );
2640 git_repository_free( repo );
2646 git_repository* repo;
2648 const std::vector<wxString>* extensions;
2650 } ctx{ repo, aDestDir, &aExtensions,
true };
2653 auto writeEntry = [](
const char* aRoot,
const git_tree_entry* aEntry,
void* aPayload ) ->
int
2655 WALK_CTX* c =
static_cast<WALK_CTX*
>( aPayload );
2657 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2660 wxString
name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2662 if( !c->extensions->empty() )
2666 for(
const wxString& ext : *c->extensions )
2668 if(
name.EndsWith( ext ) )
2679 wxString rel = wxString::FromUTF8( aRoot ) +
name;
2680 wxFileName outFn( c->destDir + wxS(
"/" ) + rel );
2682 if( !wxFileName::Mkdir( outFn.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
2688 git_blob* blob =
nullptr;
2690 if( git_blob_lookup( &blob, c->repo, git_tree_entry_id( aEntry ) ) == 0 )
2692 const void* data = git_blob_rawcontent( blob );
2693 const size_t size =
static_cast<size_t>( git_blob_rawsize( blob ) );
2694 wxFFile out( outFn.GetFullPath(), wxS(
"wb" ) );
2696 if( !( data && out.IsOpened() && out.Write( data, size ) == size ) )
2699 git_blob_free( blob );
2709 git_tree_walk( tree, GIT_TREEWALK_PRE, writeEntry, &ctx );
2711 git_tree_free( tree );
2712 git_commit_free( commit );
2713 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.
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.
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.