38#include <wx/filename.h>
42#include <wx/datetime.h>
66 const wxString& aRelativePath )
68 wxFileName fn( aRelativePath );
71 return fn.GetFullPath();
76 wxArrayString dirs = fn.GetDirs();
79 dst.AssignDir( aHistoryRoot );
81 for(
const wxString& d : dirs )
84 dst.SetFullName( fn.GetFullName() );
85 return dst.GetFullPath();
95 wxFFile fileA( aPathA, wxS(
"rb" ) );
96 wxFFile fileB( aPathB, wxS(
"rb" ) );
98 if( !fileA.IsOpened() || !fileB.IsOpened() )
101 wxFileOffset lenA = fileA.Length();
102 wxFileOffset lenB = fileB.Length();
104 if( lenA < 0 || lenB < 0 || lenA != lenB )
107 constexpr size_t chunkSize = 64 * 1024;
108 std::vector<char> bufA( chunkSize );
109 std::vector<char> bufB( chunkSize );
111 while( !fileA.Eof() )
113 size_t readA = fileA.Read( bufA.data(), chunkSize );
114 size_t readB = fileB.Read( bufB.data(), chunkSize );
119 if( readA > 0 && std::memcmp( bufA.data(), bufB.data(), readA ) != 0 )
122 if( fileA.Error() || fileB.Error() )
136 const wxString& aRelativePath,
139 wxFileName rel( aRelativePath );
141 dst.AssignDir( aAutosaveRoot );
143 for(
const wxString& d : rel.GetDirs() )
149 dst.SetFullName( rel.GetFullName() );
151 return dst.GetFullPath();
159 const wxString& aProjectPath,
160 const wxString& aAutosaveRoot,
163 wxFileName autosave( aAutosavePath );
167 wxString
name = autosave.GetFullName();
170 return wxEmptyString;
173 return autosave.GetFullPath();
176 if( !aAutosavePath.StartsWith( aAutosaveRoot ) )
177 return wxEmptyString;
179 wxString rel = aAutosavePath.Mid( aAutosaveRoot.length() );
180 wxFileName projFn( aProjectPath, wxEmptyString );
182 return projFn.GetPathWithSep() + rel;
187 const wxString& aTitle );
208 if( aProjectPath.IsEmpty() || !wxDirExists( aProjectPath ) )
211 wxDir dir( aProjectPath );
214 return dir.IsOpened()
225 return aName == wxS(
".history" ) || aName == wxS(
".git" ) || aName == wxS(
"_restore_backup" )
226 || aName.StartsWith( wxS(
"_restore_backup_" ) ) || aName == wxS(
"_restore_temp" )
241 wxFileName fn( aFile );
243 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
251 const void* aSaverObject,
252 const std::function<
void(
const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
256 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping" ), aSaverObject );
261 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)" ), aSaverObject,
m_savers.size() );
269 auto it =
m_savers.find( aSaverObject );
274 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)" ),
284 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers" ) );
289 const wxString& aTagFileType )
291 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
293 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
299 wxLogTrace(
traceAutoSave, wxS(
"[history] Backup format is ZIP; skipping git commit" ) );
306 Init( aProjectPath );
309 wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu tag='%s'" ),
310 aProjectPath, aTitle,
m_savers.size(), aTagFileType );
314 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
319 if( !aTagFileType.IsEmpty() )
325 wxLogTrace(
traceAutoSave, wxS(
"[history] previous save still in progress; skipping cycle" ) );
330 std::vector<HISTORY_FILE_DATA> fileData;
332 for(
const auto& [saverObject, saver] :
m_savers )
334 size_t before = fileData.size();
335 saver( aProjectPath, fileData );
336 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p produced %zu entries (total=%zu)" ),
337 saverObject, fileData.size() - before, fileData.size() );
343 fileData.erase( std::remove_if( fileData.begin(), fileData.end(),
346 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
348 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out entry with invalid path: '%s'" ),
349 entry.relativePath );
356 if( fileData.empty() )
358 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no entries; skipping" ) );
363 m_saveInProgress.store(
true, std::memory_order_release );
366 [
this, projectPath = aProjectPath, title = aTitle, tagFileType = aTagFileType,
367 data = std::move( fileData )]()
mutable ->
bool
369 bool result = commitInBackground( projectPath, title, data, !tagFileType.IsEmpty() );
371 if( !tagFileType.IsEmpty() )
372 TagSave( projectPath, tagFileType );
374 m_saveInProgress.store(
false, std::memory_order_release );
379 if( !aTagFileType.IsEmpty() )
380 WaitForPendingSave();
388 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
393 wxLogTrace(
traceAutoSave, wxS(
"[autosave] no savers registered; skipping" ) );
403 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create autosave root '%s'" ), autosaveRoot );
407 std::vector<HISTORY_FILE_DATA> fileData;
409 for(
const auto& [saverObject, saver] :
m_savers )
410 saver( aProjectPath, fileData );
412 bool anyWritten =
false;
416 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
420 wxFileName dstFn( dst );
424 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create dir '%s'" ), dstFn.GetPath() );
430 if( !entry.content.empty() )
432 buf = std::move( entry.content );
437 else if( !entry.sourcePath.IsEmpty() )
439 wxFFile src( entry.sourcePath, wxS(
"rb" ) );
441 if( !src.IsOpened() )
444 wxFileOffset len = src.Length();
449 buf.resize(
static_cast<size_t>( len ) );
451 if( len > 0 && src.Read( buf.data(), buf.size() ) != buf.size() )
467 wxLogTrace(
traceAutoSave, wxS(
"[autosave] wrote %zu bytes to '%s'" ), buf.size(), dst );
471 wxLogTrace(
traceAutoSave, wxS(
"[autosave] write failed for '%s': %s" ), dst, err );
484static std::vector<std::pair<wxString, wxString>>
487 std::vector<std::pair<wxString, wxString>> results;
493 if( !wxDirExists( autosaveRoot ) )
496 std::function<void(
const wxString& )> walk = [&](
const wxString& aDir )
504 bool cont = d.GetFirst( &
name );
508 wxFileName fn( aDir,
name );
509 wxString fullPath = fn.GetFullPath();
511 if( wxDirExists( fullPath ) )
514 && (
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) ) )
516 cont = d.GetNext( &
name );
529 results.emplace_back( fullPath, src );
532 cont = d.GetNext( &
name );
536 walk( autosaveRoot );
541std::vector<std::pair<wxString, wxString>>
544 std::vector<std::pair<wxString, wxString>> results;
546 if( aExtensions.empty() )
551 wxFileName srcFn( pair.second );
554 for(
const wxString& ext : aExtensions )
556 if( srcFn.GetExt().IsSameAs( ext,
false ) )
568 if( srcFn.FileExists() )
569 srcTime = srcFn.GetModificationTime();
571 wxDateTime autosaveTime = wxFileName( pair.first ).GetModificationTime();
575 bool stale = !srcTime.IsValid()
576 || ( autosaveTime.IsLaterThan( srcTime )
580 results.emplace_back( std::move( pair ) );
594 if( wxFileExists( autosavePath ) )
595 wxRemoveFile( autosavePath );
601 const std::vector<wxString>& aSourcePaths )
const
603 if( aSourcePaths.empty() )
606 std::vector<wxFileName> targets;
607 targets.reserve( aSourcePaths.size() );
609 for(
const wxString& src : aSourcePaths )
612 targets.emplace_back( src );
615 if( targets.empty() )
620 wxFileName srcFn( srcPath );
623 for(
const wxFileName& target : targets )
625 if( srcFn.SameAs( target ) )
632 if( match && wxFileExists( autosavePath ) )
633 wxRemoveFile( autosavePath );
639 const std::vector<HISTORY_FILE_DATA>& aFileData,
bool aIsManualSave )
641 wxLogTrace(
traceAutoSave, wxS(
"[history] background: writing %zu entries for '%s'" ),
642 aFileData.size(), aProjectPath );
648 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create history root '%s'" ), hist );
655 wxFileName dstFn( dst );
656 wxString parent = dstFn.GetPath();
660 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create dir '%s'" ), parent );
664 if( !entry.content.empty() )
666 std::string buf = entry.content;
671 wxFFile fp( dst, wxS(
"wb" ) );
675 fp.Write( buf.data(), buf.size() );
677 wxLogTrace(
traceAutoSave, wxS(
"[history] background: wrote %zu bytes to '%s'" ), buf.size(), dst );
681 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to open '%s' for writing" ), dst );
684 else if( !entry.sourcePath.IsEmpty() )
686 wxCopyFile( entry.sourcePath, dst,
true );
687 wxLogTrace(
traceAutoSave, wxS(
"[history] background: copied '%s' -> '%s'" ), entry.sourcePath, dst );
703 git_repository_set_workdir( repo, hist.mb_str().data(),
false );
709 wxString rel = entry.relativePath;
710 rel.Replace( wxS(
"\\" ), wxS(
"/" ) );
714 if( !wxFileExists( abs ) )
717 git_index_add_bypath(
index, rel.ToStdString().c_str() );
722 git_commit* head_commit =
nullptr;
723 git_tree* head_tree =
nullptr;
725 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
726 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
727 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
729 git_tree* rawIndexTree =
nullptr;
730 git_oid index_tree_oid;
732 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
735 git_tree_free( head_tree );
738 git_commit_free( head_commit );
740 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to write index tree" ) );
744 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
745 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
747 bool hasChanges =
true;
751 git_diff* diff =
nullptr;
753 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
755 hasChanges = git_diff_num_deltas( diff ) > 0;
756 wxLogTrace(
traceAutoSave, wxS(
"[history] background: diff deltas=%u" ),
757 (
unsigned) git_diff_num_deltas( diff ) );
758 git_diff_free( diff );
765 bool stagedMatchesDisk =
true;
769 wxString diskPath = aProjectPath + wxFileName::GetPathSeparator() + entry.relativePath;
772 if( !wxFileExists( diskPath ) || !wxFileExists( histPath ) )
774 stagedMatchesDisk =
false;
778 wxFFile diskFile( diskPath, wxT(
"rb" ) );
779 wxFFile histFile( histPath, wxT(
"rb" ) );
781 if( !diskFile.IsOpened() || !histFile.IsOpened() || diskFile.Length() != histFile.Length() )
783 stagedMatchesDisk =
false;
787 size_t len =
static_cast<size_t>( diskFile.Length() );
788 std::string diskBuf( len,
'\0' );
789 std::string histBuf( len,
'\0' );
791 if( diskFile.Read( diskBuf.data(), len ) != len
792 || histFile.Read( histBuf.data(), len ) != len
793 || diskBuf != histBuf )
795 stagedMatchesDisk =
false;
800 if( stagedMatchesDisk && !aIsManualSave )
802 wxLogTrace(
traceAutoSave, wxS(
"[history] background: first commit; staged matches disk -- skipping" ) );
808 git_tree_free( head_tree );
811 git_commit_free( head_commit );
815 wxLogTrace(
traceAutoSave, wxS(
"[history] background: no changes detected; no commit") );
819 if( !aTitle.IsEmpty() && aTitle != wxS(
"Autosave" ) )
821 git_oid head_oid_amend;
823 if( git_reference_name_to_id( &head_oid_amend, repo,
"HEAD" ) == 0 )
825 git_commit* head_commit_amend =
nullptr;
827 if( git_commit_lookup( &head_commit_amend, repo, &head_oid_amend ) == 0 )
829 wxString existingMsg = wxString::FromUTF8( git_commit_message( head_commit_amend ) );
830 existingMsg.Trim(
true ).Trim(
false );
832 if( existingMsg != aTitle )
835 int amend_rc = git_commit_amend( &amended_oid, head_commit_amend,
"HEAD",
nullptr,
nullptr,
836 nullptr, aTitle.mb_str().data(),
nullptr );
839 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amended HEAD message '%s' -> '%s'" ),
840 existingMsg, aTitle );
842 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amend failed rc=%d" ), amend_rc );
845 git_commit_free( head_commit_amend );
853 git_signature* rawSig =
nullptr;
855 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
857 git_commit* parent =
nullptr;
861 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
863 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
867 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
869 const git_commit* constParent = parent;
871 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
872 msg.mb_str().data(), indexTree.get(), parents,
873 parents ? &constParent :
nullptr );
877 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit created %s (%s entries=%zu)" ),
878 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
882 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit failed rc=%d" ), rc );
886 git_commit_free( parent );
888 git_index_write(
index );
897 wxLogTrace(
traceAutoSave, wxS(
"[history] waiting for pending background save" ) );
921 if( !wxDirExists( hist ) )
930 git_repository* rawRepo =
nullptr;
932 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
934 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
937 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
938 if( !ignoreFile.FileExists() )
940 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
943 f.Write( wxS(
"# KiCad local history exclusions. Edit to add your own rules.\n"
950 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
952 if( !readmeFile.FileExists() )
954 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
958 f.Write( wxS(
"KiCad Local History Directory\n"
959 "=============================\n\n"
960 "This directory contains automatic snapshots of your project files.\n"
961 "KiCad periodically saves copies of your work here, allowing you to\n"
962 "recover from accidental changes or data loss.\n\n"
963 "You can browse and restore previous versions through KiCad's\n"
964 "File > Local History menu.\n\n"
965 "To disable this feature:\n"
966 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
967 "This directory can be safely deleted if you no longer need the\n"
968 "history, but doing so will permanently remove all saved snapshots.\n" ) );
974 git_repository_free( rawRepo );
990 const wxString& aHistoryPath,
const wxString& aProjectPath,
991 const std::vector<wxString>& aFiles,
const wxString& aTitle )
993 std::vector<std::string> filesArrStr;
995 for(
const wxString& file : aFiles )
997 wxFileName src( file );
1000 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
1001 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
1003 relPath = src.GetFullName();
1005 relPath.Replace(
"\\",
"/" );
1006 std::string relPathStr = relPath.ToStdString();
1008 unsigned int status = 0;
1009 int rc = git_status_file( &status, repo, relPathStr.data() );
1011 if( rc == 0 && status != 0 )
1013 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
1014 filesArrStr.emplace_back( relPathStr );
1018 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
1019 filesArrStr.emplace_back( relPathStr );
1023 std::vector<char*> cStrings( filesArrStr.size() );
1025 for(
size_t i = 0; i < filesArrStr.size(); i++ )
1026 cStrings[i] = filesArrStr[i].data();
1028 git_strarray filesArrGit;
1029 filesArrGit.count = filesArrStr.size();
1030 filesArrGit.strings = cStrings.data();
1032 if( filesArrStr.size() == 0 )
1038 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
1040 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
1046 if( git_index_write_tree( &tree_id,
index ) != 0 )
1049 git_tree* rawTree =
nullptr;
1050 git_tree_lookup( &rawTree, repo, &tree_id );
1051 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
1053 git_signature* rawSig =
nullptr;
1055 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
1056 &git_signature_free );
1058 git_commit* rawParent =
nullptr;
1062 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
1064 git_commit_lookup( &rawParent, repo, &parent_id );
1068 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
1071 git_tree* rawParentTree =
nullptr;
1074 git_commit_tree( &rawParentTree, parent.get() );
1076 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
1078 git_diff* rawDiff =
nullptr;
1079 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
1080 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
1082 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
1084 if( numChangedFiles == 0 )
1086 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
1092 if( !aTitle.IsEmpty() )
1093 msg << aTitle << wxS(
": " );
1095 msg << numChangedFiles << wxS(
" files changed" );
1097 for(
size_t i = 0; i < numChangedFiles; ++i )
1099 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
1100 git_patch* rawPatch =
nullptr;
1101 git_patch_from_diff( &rawPatch, diff.get(), i );
1102 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
1104 size_t context = 0, adds = 0, dels = 0;
1105 git_patch_line_stats( &context, &adds, &dels, patch.get() );
1106 size_t updated = std::min( adds, dels );
1109 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
1110 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
1114 git_commit* parentPtr = parent.get();
1115 const git_commit* constParentPtr = parentPtr;
1116 if( git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr, msg.mb_str().data(), tree.get(),
1117 parents, parentPtr ? &constParentPtr :
nullptr )
1123 git_index_write(
index );
1132 const wxString& aTitle )
1140 wxLogTrace(
traceAutoSave, wxS(
"[history] commitSnapshotForProject failed to acquire lock: %s" ),
1152 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled
1158 wxString proj = wxFileName( aFiles[0] ).GetPath();
1172 wxString
name = aFile.GetFullName();
1174 if(
name == wxS(
"sym-lib-table" ) ||
name == wxS(
"fp-lib-table" ) )
1177 return aFile.GetExt().StartsWith( wxS(
"kicad_" ) );
1186 wxDir dir( aProjectPath );
1188 if( !dir.IsOpened() )
1192 std::function<void(
const wxString&,
bool )> collect =
1193 [&](
const wxString&
path,
bool topLevel )
1198 wxS(
"[history] collectProjectFiles: Skipping nested project at %s" ),
1209 bool cont = d.GetFirst( &
name );
1215 cont = d.GetNext( &
name );
1220 wxString fullPath = fn.GetFullPath();
1222 if( wxFileName::DirExists( fullPath ) )
1224 collect( fullPath,
false );
1226 else if( fn.FileExists() && fn.GetFullName() != wxS(
"fp-info-cache" ) &&
isKiCadProjectFile( fn ) )
1228 aFiles.push_back( fn.GetFullPath() );
1231 cont = d.GetNext( &
name );
1235 collect( aProjectPath,
true );
1246 wxLogTrace(
traceAutoSave, wxS(
"[history] Backup format is ZIP; skipping full snapshot" ) );
1250 std::vector<wxString> files;
1256 Init( aProjectPath );
1262 return wxDirExists(
historyPath( aProjectPath ) );
1274 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
1279 git_reference* ref =
nullptr;
1282 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
1283 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
1286 git_object* head_obj =
nullptr;
1287 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
1288 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
1289 git_object_free( head_obj );
1292 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
1293 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
1295 git_reference_delete( ref );
1296 git_reference_free( ref );
1299 git_oid last_tag_oid;
1300 git_object* head_obj2 =
nullptr;
1301 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
1302 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
1303 git_object_free( head_obj2 );
1321 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
1331 git_repository* repo =
nullptr;
1333 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1337 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1339 git_repository_free( repo );
1343 git_commit* head_commit =
nullptr;
1344 git_commit_lookup( &head_commit, repo, &head_oid );
1345 git_time_t head_time = git_commit_time( head_commit );
1348 git_tag_list_match( &tags,
"Last_Save_*", repo );
1349 git_time_t save_time = 0;
1351 for(
size_t i = 0; i < tags.count; ++i )
1353 git_reference* ref =
nullptr;
1354 if( git_reference_lookup( &ref, repo,
1355 ( wxS(
"refs/tags/" ) +
1356 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
1358 const git_oid* oid = git_reference_target( ref );
1359 git_commit* c =
nullptr;
1360 if( git_commit_lookup( &c, repo, oid ) == 0 )
1362 git_time_t t = git_commit_time( c );
1365 git_commit_free( c );
1367 git_reference_free( ref );
1371 git_strarray_free( &tags );
1372 git_commit_free( head_commit );
1373 git_repository_free( repo );
1377 if( save_time == 0 )
1380 return head_time > save_time;
1384 const wxString& aMessage )
1396 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
1405 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
1406 git_reference* lastRef =
nullptr;
1407 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
1409 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
1411 const git_oid* lastOid = git_reference_target( lastRef );
1412 git_commit* lastCommit =
nullptr;
1413 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
1415 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
1417 git_tree* lastTree =
nullptr;
1418 git_commit_tree( &lastTree, lastCommit );
1419 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
1423 git_commit* headCommit =
nullptr;
1425 const git_commit* parentArray[1];
1426 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
1427 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
1429 parentArray[0] = headCommit;
1433 git_signature* sigRaw =
nullptr;
1435 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
1437 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
1438 git_oid newCommitOid;
1439 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
1440 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
1441 if( headCommit ) git_commit_free( headCommit );
1446 git_reference* existing =
nullptr;
1447 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
1449 git_reference_delete( existing );
1450 git_reference_free( existing );
1452 git_object* newCommitObj =
nullptr;
1453 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
1455 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
1456 git_object_free( newCommitObj );
1465 if( !dir.IsOpened() )
1468 bool cont = dir.GetFirst( &
name );
1472 wxString fullPath = fn.GetFullPath();
1474 if( wxFileName::DirExists( fullPath ) )
1476 else if( fn.FileExists() )
1477 total += (size_t) fn.GetSize().GetValue();
1478 cont = dir.GetNext( &
name );
1484static bool copyTreeObjects( git_repository* aSrcRepo, git_odb* aSrcOdb, git_odb* aDstOdb,
const git_oid* aTreeOid,
1485 std::set<git_oid,
bool ( * )(
const git_oid&,
const git_oid& )>& aCopied )
1487 if( aCopied.count( *aTreeOid ) )
1490 git_odb_object* obj =
nullptr;
1492 if( git_odb_read( &obj, aSrcOdb, aTreeOid ) != 0 )
1496 int err = git_odb_write( &written, aDstOdb, git_odb_object_data( obj ), git_odb_object_size( obj ),
1497 git_odb_object_type( obj ) );
1498 git_odb_object_free( obj );
1503 aCopied.insert( *aTreeOid );
1505 git_tree* tree =
nullptr;
1507 if( git_tree_lookup( &tree, aSrcRepo, aTreeOid ) != 0 )
1510 size_t cnt = git_tree_entrycount( tree );
1512 for(
size_t i = 0; i < cnt; ++i )
1514 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1515 const git_oid* entryId = git_tree_entry_id( entry );
1517 if( aCopied.count( *entryId ) )
1520 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1522 if( !
copyTreeObjects( aSrcRepo, aSrcOdb, aDstOdb, entryId, aCopied ) )
1524 git_tree_free( tree );
1528 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1530 git_odb_object* blobObj =
nullptr;
1532 if( git_odb_read( &blobObj, aSrcOdb, entryId ) == 0 )
1534 git_oid blobWritten;
1536 if( git_odb_write( &blobWritten, aDstOdb, git_odb_object_data( blobObj ),
1537 git_odb_object_size( blobObj ), git_odb_object_type( blobObj ) )
1540 git_odb_object_free( blobObj );
1541 git_tree_free( tree );
1545 git_odb_object_free( blobObj );
1546 aCopied.insert( *entryId );
1551 git_tree_free( tree );
1560 git_packbuilder* pb =
nullptr;
1562 if( git_packbuilder_new( &pb, aRepo ) != 0 )
1565 git_revwalk* walk =
nullptr;
1567 if( git_revwalk_new( &walk, aRepo ) != 0 )
1569 git_packbuilder_free( pb );
1573 git_revwalk_push_head( walk );
1576 while( git_revwalk_next( &oid, walk ) == 0 )
1578 if( git_packbuilder_insert_commit( pb, &oid ) != 0 )
1580 git_revwalk_free( walk );
1581 git_packbuilder_free( pb );
1586 git_revwalk_free( walk );
1590 git_packbuilder_set_callbacks(
1592 [](
int aStage, uint32_t aCurrent, uint32_t aTotal,
void* aPayload )
1597 reporter->SetCurrentProgress( (
double) aCurrent / aTotal );
1605 if( git_packbuilder_write( pb,
nullptr, 0,
nullptr,
nullptr ) != 0 )
1607 git_packbuilder_free( pb );
1611 git_packbuilder_free( pb );
1613 wxString objPath = wxString::FromUTF8( git_repository_path( aRepo ) ) + wxS(
"objects" );
1614 wxDir objDir( objPath );
1616 if( objDir.IsOpened() )
1618 wxArrayString toRemove;
1620 bool cont = objDir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS );
1624 if(
name.length() == 2 )
1625 toRemove.Add( objPath + wxFileName::GetPathSeparator() +
name );
1627 cont = objDir.GetNext( &
name );
1630 for(
const wxString& dir : toRemove )
1631 wxFileName::Rmdir( dir, wxPATH_RMDIR_RECURSIVE );
1640 if( aMaxBytes == 0 )
1645 if( !wxDirExists( hist ) )
1650 if( current <= aMaxBytes )
1657 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
1667 aReporter->
Report(
_(
"Compacting local history..." ) );
1674 if( current <= aMaxBytes )
1678 git_revwalk* walk =
nullptr;
1679 git_revwalk_new( &walk, repo );
1680 git_revwalk_sorting( walk, GIT_SORT_TIME );
1681 git_revwalk_push_head( walk );
1682 std::vector<git_oid> commits;
1685 while( git_revwalk_next( &oid, walk ) == 0 )
1686 commits.push_back( oid );
1688 git_revwalk_free( walk );
1690 if( commits.empty() )
1694 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
1695 [](
const git_oid& a,
const git_oid& b )
1697 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1700 size_t keptBytes = 0;
1701 std::vector<git_oid> keep;
1703 git_odb* odb =
nullptr;
1704 git_repository_odb( &odb, repo );
1706 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
1709 size_t cnt = git_tree_entrycount( tree );
1711 for(
size_t i = 0; i < cnt; ++i )
1713 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1715 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1717 const git_oid* bid = git_tree_entry_id( entry );
1719 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1722 git_object_t type = GIT_OBJECT_ANY;
1724 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1727 seenBlobs.insert( *bid );
1730 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1732 git_tree* sub =
nullptr;
1734 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1736 added += accountTree( sub );
1737 git_tree_free( sub );
1745 for(
const git_oid& cOid : commits )
1747 git_commit* c =
nullptr;
1749 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1752 git_tree* tree =
nullptr;
1753 git_commit_tree( &tree, c );
1754 size_t add = accountTree( tree );
1755 git_tree_free( tree );
1756 git_commit_free( c );
1758 if( keep.empty() || keptBytes + add <= aMaxBytes )
1760 keep.push_back( cOid );
1768 keep.push_back( commits.front() );
1772 std::vector<std::pair<wxString, git_oid>> tagTargets;
1773 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
1774 [](
const git_oid& a,
const git_oid& b )
1776 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1778 git_strarray tagList;
1780 if( git_tag_list( &tagList, repo ) == 0 )
1782 for(
size_t i = 0; i < tagList.count; ++i )
1784 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1785 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1787 git_reference* tref =
nullptr;
1789 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1791 const git_oid* toid = git_reference_target( tref );
1795 tagTargets.emplace_back(
name, *toid );
1796 taggedCommits.insert( *toid );
1800 for(
const auto& k : keep )
1802 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1812 keep.push_back( *toid );
1813 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1818 git_reference_free( tref );
1822 git_strarray_free( &tagList );
1826 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1827 wxString trimPath = trimFn.GetPath();
1829 if( wxDirExists( trimPath ) )
1830 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1832 wxMkdir( trimPath );
1833 git_repository* newRepo =
nullptr;
1835 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1837 git_odb_free( odb );
1841 git_odb* dstOdb =
nullptr;
1843 if( git_repository_odb( &dstOdb, newRepo ) != 0 )
1845 git_repository_free( newRepo );
1846 git_odb_free( odb );
1850 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> copiedObjects(
1851 [](
const git_oid& a,
const git_oid& b )
1853 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1857 std::reverse( keep.begin(), keep.end() );
1858 git_commit* parent =
nullptr;
1859 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1860 std::vector<MAP_ENTRY> commitMap;
1864 aReporter->
AdvancePhase(
_(
"Trimming local history..." ) );
1868 for(
size_t idx = 0; idx < keep.size(); ++idx )
1873 const git_oid& co = keep[idx];
1874 git_commit* orig =
nullptr;
1876 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1879 git_tree* tree =
nullptr;
1880 git_commit_tree( &tree, orig );
1882 copyTreeObjects( repo, odb, dstOdb, git_tree_id( tree ), copiedObjects );
1884 git_tree* newTree =
nullptr;
1885 git_tree_lookup( &newTree, newRepo, git_tree_id( tree ) );
1887 git_tree_free( tree );
1890 const git_signature* origAuthor = git_commit_author( orig );
1891 const git_signature* origCommitter = git_commit_committer( orig );
1892 git_signature* sigAuthor =
nullptr;
1893 git_signature* sigCommitter =
nullptr;
1895 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1896 origAuthor->when.time, origAuthor->when.offset );
1897 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1898 origCommitter->when.time, origCommitter->when.offset );
1900 const git_commit* parents[1];
1901 int parentCount = 0;
1905 parents[0] = parent;
1909 git_oid newCommitOid;
1910 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1911 newTree, parentCount, parentCount ? parents :
nullptr );
1914 git_commit_free( parent );
1916 git_commit_lookup( &parent, newRepo, &newCommitOid );
1918 commitMap.emplace_back( co, newCommitOid );
1920 git_signature_free( sigAuthor );
1921 git_signature_free( sigCommitter );
1922 git_tree_free( newTree );
1923 git_commit_free( orig );
1927 git_commit_free( parent );
1930 for(
const auto& tt : tagTargets )
1933 const git_oid* newOid =
nullptr;
1935 for(
const auto& m : commitMap )
1937 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1947 git_object* obj =
nullptr;
1949 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1951 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1952 git_object_free( obj );
1957 aReporter->
AdvancePhase(
_(
"Compacting trimmed history..." ) );
1964 git_odb_free( dstOdb );
1965 git_odb_free( odb );
1966 git_repository_free( newRepo );
1971 wxString backupOld = hist + wxS(
"_old");
1972 wxRenameFile( hist, backupOld );
1973 wxRenameFile( trimPath, hist );
1974 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1981 git_repository* repo =
nullptr;
1983 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1984 return wxEmptyString;
1987 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1989 git_repository_free( repo );
1990 return wxEmptyString;
1993 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1994 git_repository_free( repo );
2006bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
2008 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
2010 wxDir dir( dirPath );
2011 if( !dir.IsOpened() )
2015 bool cont = dir.GetFirst( &filename );
2019 wxFileName fullPath( dirPath, filename );
2022 if( filename == wxS(
".history") || filename == wxS(
".git") )
2024 cont = dir.GetNext( &filename );
2028 if( fullPath.DirExists() )
2030 findLocks( fullPath.GetFullPath() );
2032 else if( fullPath.FileExists()
2039 baseName = baseName.BeforeLast(
'.' );
2040 wxFileName originalFile( dirPath, baseName );
2043 LOCKFILE testLock( originalFile.GetFullPath() );
2044 if( testLock.Valid() && !testLock.IsLockedByMe() )
2046 aLockedFiles.push_back( fullPath.GetFullPath() );
2050 cont = dir.GetNext( &filename );
2054 findLocks( aProjectPath );
2055 return aLockedFiles.empty();
2062bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
2064 bool extractSuccess =
true;
2066 std::function<void( git_tree*,
const wxString& )> extractTree =
2067 [&]( git_tree* t,
const wxString& prefix )
2069 if( !extractSuccess )
2072 size_t cnt = git_tree_entrycount( t );
2073 for(
size_t i = 0; i < cnt; ++i )
2075 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
2076 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
2077 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
2079 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
2081 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
2083 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
2086 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
2087 dirPath.GetPath() );
2088 extractSuccess =
false;
2092 git_tree* sub =
nullptr;
2093 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
2095 extractTree( sub, fullPath );
2096 git_tree_free( sub );
2099 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
2101 git_blob* blob =
nullptr;
2102 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
2104 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
2106 wxFileName dstDir( dst );
2107 dstDir.SetFullName( wxEmptyString );
2108 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
2110 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
2113 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
2119 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
2120 dst.GetFullPath() );
2121 extractSuccess =
false;
2122 git_blob_free( blob );
2126 git_blob_free( blob );
2132 extractTree( aTree, wxEmptyString );
2133 return extractSuccess;
2143void collectRelativeFiles(
const wxString& aRoot,
const wxString& aDir, std::vector<wxString>& aOut )
2147 if( !dir.IsOpened() )
2152 for(
bool cont = dir.GetFirst( &
name ); cont; cont = dir.GetNext( &
name ) )
2154 wxString full = aDir + wxFILE_SEP_PATH +
name;
2156 if( wxDirExists( full ) )
2158 collectRelativeFiles( aRoot, full, aOut );
2160 else if( wxFileExists( full ) )
2162 wxString rel = full.Mid( aRoot.length() + 1 );
2163 rel.Replace( wxS(
"\\" ), wxS(
"/" ) );
2164 aOut.push_back( rel );
2178bool overlaySnapshotFiles(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
const wxString& aBackupPath )
2180 std::vector<wxString> relPaths;
2181 collectRelativeFiles( aTempRestorePath, aTempRestorePath, relPaths );
2183 for(
const wxString& rel : relPaths )
2185 wxFileName src( aTempRestorePath + wxFILE_SEP_PATH + rel );
2186 wxFileName dst( aProjectPath + wxFILE_SEP_PATH + rel );
2188 if( dst.FileExists() )
2190 wxFileName bak( aBackupPath + wxFILE_SEP_PATH + rel );
2192 if( !wxFileName::Mkdir( bak.GetPath(), 0777, wxPATH_MKDIR_FULL )
2193 || !wxCopyFile( dst.GetFullPath(), bak.GetFullPath(),
true ) )
2199 if( !wxFileName::Mkdir( dst.GetPath(), 0777, wxPATH_MKDIR_FULL )
2200 || !wxCopyFile( src.GetFullPath(), dst.GetFullPath(),
true ) )
2217 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
2220 std::vector<wxString> lockedFiles;
2221 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
2224 for(
const auto& f : lockedFiles )
2225 lockList += wxS(
"\n - ") + f;
2228 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
2234 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
2236 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
2247 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
2258 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
2260 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
2264 git_commit* commit =
nullptr;
2265 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2267 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
2271 git_tree* tree =
nullptr;
2272 git_commit_tree( &tree, commit );
2276 if( aConfirm && aParent )
2278 wxDateTime when( (time_t) git_commit_time( commit ) );
2281 wxString::Format(
_(
"Restore the project to the version from %s?" ),
2282 when.Format( wxS(
"%Y-%m-%d %H:%M:%S" ) ) ),
2283 _(
"Restore Version" ), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION );
2285 dlg.SetYesNoLabels(
_(
"Restore" ),
_(
"Cancel" ) );
2286 dlg.SetExtendedMessage(
_(
"Your current files are backed up first so you can undo the "
2287 "restore. Files that are not part of this version are left "
2290 if( dlg.ShowModal() != wxID_YES )
2292 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: User cancelled at confirm" ) );
2293 git_tree_free( tree );
2294 git_commit_free( commit );
2300 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
2302 std::vector<wxString> backupFiles;
2305 if( !backupFiles.empty() )
2309 backupFiles, wxS(
"Pre-restore backup" ) );
2314 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
2315 git_tree_free( tree );
2316 git_commit_free( commit );
2322 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Current state already matches HEAD; "
2323 "continuing without a new backup commit" ) );
2328 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
2330 if( wxDirExists( tempRestorePath ) )
2331 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2333 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
2336 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
2338 git_tree_free( tree );
2339 git_commit_free( commit );
2343 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
2346 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
2348 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
2349 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2350 git_tree_free( tree );
2351 git_commit_free( commit );
2360 wxString backupPath =
2361 aProjectPath + wxS(
"_restore_backup_" )
2362 + wxDateTime::UNow().Format( wxS(
"%Y-%m-%dT%H-%M-%S-%l" ) );
2364 if( !overlaySnapshotFiles( tempRestorePath, aProjectPath, backupPath ) )
2366 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Overlay failed, rolling back from backup" ) );
2369 if( wxDirExists( backupPath ) )
2371 wxString discard = aProjectPath + wxS(
"_restore_discard" );
2372 overlaySnapshotFiles( backupPath, aProjectPath, discard );
2373 wxFileName::Rmdir( discard, wxPATH_RMDIR_RECURSIVE );
2374 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
2377 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2378 git_tree_free( tree );
2379 git_commit_free( commit );
2385 wxS(
"[history] RestoreCommit: Restore successful, backup retained at %s" ),
2387 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2390 std::vector<wxString> resultFiles;
2393 wxString::Format( wxS(
"Restored from %s" ), aHash ) );
2398 git_tree_free( tree );
2399 git_commit_free( commit );
2401 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
2410 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots =
LoadSnapshots( aProjectPath );
2412 if( snapshots.empty() )
2421 if( !selectedHash.IsEmpty() )
2428 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2431 git_repository* repo =
nullptr;
2433 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2436 git_revwalk* walk =
nullptr;
2437 if( git_revwalk_new( &walk, repo ) != 0 )
2439 git_repository_free( repo );
2443 git_revwalk_sorting( walk, GIT_SORT_TIME );
2444 git_revwalk_push_head( walk );
2448 while( git_revwalk_next( &oid, walk ) == 0 )
2450 git_commit* commit =
nullptr;
2452 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2456 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2457 info.date = wxDateTime(
static_cast<time_t
>( git_commit_time( commit ) ) );
2458 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2460 wxString firstLine =
info.message.BeforeFirst(
'\n' );
2462 long parsedCount = 0;
2464 firstLine.BeforeFirst(
':', &remainder );
2465 remainder.Trim(
true ).Trim(
false );
2467 if( remainder.EndsWith( wxS(
"files changed" ) ) )
2469 wxString countText = remainder.BeforeFirst(
' ' );
2471 if( countText.ToLong( &parsedCount ) )
2472 info.filesChanged =
static_cast<int>( parsedCount );
2475 info.summary = firstLine.BeforeFirst(
':' );
2478 info.message.BeforeFirst(
'\n', &rest );
2479 wxArrayString lines = wxSplit( rest,
'\n',
'\0' );
2481 for(
const wxString& line : lines )
2483 if( !line.IsEmpty() )
2484 info.changedFiles.Add( line );
2487 snapshots.push_back( std::move(
info ) );
2488 git_commit_free( commit );
2491 git_revwalk_free( walk );
2492 git_repository_free( repo );
2504 const wxString& aExtension )
2507 git_repository* repo =
nullptr;
2509 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2510 return wxEmptyString;
2513 git_commit* commit =
nullptr;
2514 git_tree* tree =
nullptr;
2516 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2518 git_repository_free( repo );
2519 return wxEmptyString;
2522 if( git_commit_tree( &tree, commit ) != 0 )
2524 git_commit_free( commit );
2525 git_repository_free( repo );
2526 return wxEmptyString;
2532 std::vector<wxString> entries;
2533 } ctx{ aExtension, {} };
2535 auto collect = [](
const char* aRoot,
const git_tree_entry* aEntry,
void* aPayload ) ->
int
2537 WALK_CTX* c =
static_cast<WALK_CTX*
>( aPayload );
2539 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2542 wxString
name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2544 if( !
name.EndsWith( c->ext ) )
2547 wxString
path = wxString::FromUTF8( aRoot ) +
name;
2548 c->entries.push_back(
path + wxS(
":" )
2549 + wxString::FromUTF8( git_oid_tostr_s( git_tree_entry_id( aEntry ) ) ) );
2553 git_tree_walk( tree, GIT_TREEWALK_PRE, collect, &ctx );
2555 git_tree_free( tree );
2556 git_commit_free( commit );
2557 git_repository_free( repo );
2559 std::sort( ctx.entries.begin(), ctx.entries.end() );
2561 wxString fingerprint;
2563 for(
const wxString& entry : ctx.entries )
2564 fingerprint << entry << wxS(
"|" );
2571 const wxString& aDestDir,
const std::vector<wxString>& aExtensions )
2574 git_repository* repo =
nullptr;
2576 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2580 git_commit* commit =
nullptr;
2581 git_tree* tree =
nullptr;
2583 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2585 git_repository_free( repo );
2589 if( git_commit_tree( &tree, commit ) != 0 )
2591 git_commit_free( commit );
2592 git_repository_free( repo );
2598 git_repository* repo;
2600 const std::vector<wxString>* extensions;
2602 } ctx{ repo, aDestDir, &aExtensions,
true };
2605 auto writeEntry = [](
const char* aRoot,
const git_tree_entry* aEntry,
void* aPayload ) ->
int
2607 WALK_CTX* c =
static_cast<WALK_CTX*
>( aPayload );
2609 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2612 wxString
name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2614 if( !c->extensions->empty() )
2618 for(
const wxString& ext : *c->extensions )
2620 if(
name.EndsWith( ext ) )
2631 wxString rel = wxString::FromUTF8( aRoot ) +
name;
2632 wxFileName outFn( c->destDir + wxS(
"/" ) + rel );
2634 if( !wxFileName::Mkdir( outFn.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
2640 git_blob* blob =
nullptr;
2642 if( git_blob_lookup( &blob, c->repo, git_tree_entry_id( aEntry ) ) == 0 )
2644 const void* data = git_blob_rawcontent( blob );
2645 const size_t size =
static_cast<size_t>( git_blob_rawsize( blob ) );
2646 wxFFile out( outFn.GetFullPath(), wxS(
"wb" ) );
2648 if( !( data && out.IsOpened() && out.Write( data, size ) == size ) )
2651 git_blob_free( blob );
2661 git_tree_walk( tree, GIT_TREEWALK_PRE, writeEntry, &ctx );
2663 git_tree_free( tree );
2664 git_commit_free( commit );
2665 git_repository_free( repo );
wxString GetSelectedHash() const
Hybrid locking mechanism for local history git repositories.
git_repository * GetRepository()
Get the git repository handle (only valid if IsLocked() returns true).
void ReleaseRepository()
Release git repository and index handles early, but keep the file lock.
wxString GetLockError() const
Get error message describing why lock could not be acquired.
git_index * GetIndex()
Get the git index handle (only valid if IsLocked() returns true).
bool IsLocked() const
Check if locks were successfully acquired.
std::vector< LOCAL_HISTORY_SNAPSHOT_INFO > LoadSnapshots(const wxString &aProjectPath)
bool EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes, PROGRESS_REPORTER *aReporter=nullptr)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
bool TagSave(const wxString &aProjectPath, const wxString &aFileType)
Tag a manual save in the local history repository.
bool RunRegisteredSaversAndCommit(const wxString &aProjectPath, const wxString &aTitle, const wxString &aTagFileType=wxEmptyString)
Run all registered savers and, if any staged changes differ from HEAD, create a commit.
std::vector< std::pair< wxString, wxString > > FindStaleAutosaveFiles(const wxString &aProjectPath, const std::vector< wxString > &aExtensions) const
Enumerate autosave files newer than their corresponding source files for the project at aProjectPath,...
wxString GetHeadHash(const wxString &aProjectPath)
Return the current head commit hash.
bool commitInBackground(const wxString &aProjectPath, const wxString &aTitle, const std::vector< HISTORY_FILE_DATA > &aFileData, bool aIsManualSave)
Execute file writes and git commit on a background thread.
void ShowRestoreDialog(const wxString &aProjectPath, wxWindow *aParent)
Show a dialog allowing the user to choose a snapshot to restore.
bool HeadNewerThanLastSave(const wxString &aProjectPath)
Return true if the autosave data is newer than the last manual save.
std::set< wxString > m_pendingFiles
std::map< const void *, std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> > m_savers
bool CommitDuplicateOfLastSave(const wxString &aProjectPath, const wxString &aFileType, const wxString &aMessage)
Create a new commit duplicating the tree pointed to by Last_Save_<fileType> and move the Last_Save_<f...
void WaitForPendingSave()
Block until any pending background save completes.
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.
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> &aSaver)
Register a saver callback invoked during autosave history commits.
bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
void ClearAllSavers()
Clear all registered savers.
bool CommitSnapshot(const std::vector< wxString > &aFiles, const wxString &aTitle)
Commit the given files to the local history repository.
std::atomic< bool > m_saveInProgress
void NoteFileChange(const wxString &aFile)
Record that a file has been modified and should be included in the next snapshot.
bool CommitPending()
Commit any pending modified files to the history repository.
bool HistoryExists(const wxString &aProjectPath)
Return true if history exists for the project.
bool RunRegisteredSaversAsAutosaveFiles(const wxString &aProjectPath)
Run all registered savers and write their output to autosave files instead of committing to the local...
bool CommitFullProjectSnapshot(const wxString &aProjectPath, const wxString &aTitle)
Commit a snapshot of the entire project directory (excluding the .history directory and ignored trans...
std::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 UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
void RemoveAutosaveFiles(const wxString &aProjectPath) const
Remove every autosave file under the project at aProjectPath regardless of which source it shadowed.
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
virtual COMMON_SETTINGS * GetCommonSettings() const
virtual SETTINGS_MANAGER & GetSettingsManager() const
A progress reporter interface for use in multi-threaded environments.
virtual void Report(const wxString &aMessage)=0
Display aMessage in the progress bar dialog.
virtual void AdvancePhase()=0
Use the next available virtual zone of the dialog progress bar.
virtual void SetCurrentProgress(double aProgress)=0
Set the progress value to aProgress (0..1).
COMMON_SETTINGS * GetCommonSettings() const
Retrieve the common settings shared by all applications.
wxString GetAutosaveRootForProject(const PROJECT *aProject=nullptr) const
Resolve the autosave-files root for a project.
PROJECT * GetProjectForPath(const wxString &aProjectPath) const
Return the active project iff its path matches aProjectPath, else nullptr.
wxString GetLocalHistoryDirForPath(const wxString &aProjectPath) const
Resolve the local-history directory for a project given by its on-disk path.
@ INCREMENTAL
Git-based local history (default)
@ PROJECT_DIR
Inside the project directory (default)
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
static const std::string LockFileExtension
static const std::string ProjectFileExtension
static const std::string LockFilePrefix
const wxChar *const traceAutoSave
Flag to enable auto save feature debug tracing.
static wxString historyPath(const wxString &aProjectPath)
static wxString historyPath(const wxString &aProjectPath)
static bool compactRepository(git_repository *aRepo, PROGRESS_REPORTER *aReporter=nullptr)
static bool isRestoreProtectedEntry(const wxString &aName)
static std::vector< std::pair< wxString, wxString > > findAutosaveFilePairs(const wxString &aProjectPath)
static const wxString AUTOSAVE_PREFIX
static bool commitSnapshotForProject(const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
static size_t dirSizeRecursive(const wxString &path)
static bool copyTreeObjects(git_repository *aSrcRepo, git_odb *aSrcOdb, git_odb *aDstOdb, const git_oid *aTreeOid, std::set< git_oid, bool(*)(const git_oid &, const git_oid &)> &aCopied)
static bool isKiCadProjectFile(const wxFileName &aFile)
static wxString sourceForAutosaveFile(const wxString &aAutosavePath, const wxString &aProjectPath, const wxString &aAutosaveRoot, BACKUP_LOCATION aLocation)
static 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 filesContentEqual(const wxString &aPathA, const wxString &aPathB)
static bool formatUsesIncrementalHistory()
static bool isProjectDirectory(const wxString &aProjectPath)
static void collectProjectFiles(const wxString &aProjectPath, std::vector< wxString > &aFiles)
static wxString joinHistoryDestination(const wxString &aHistoryRoot, const wxString &aRelativePath)
PGM_BASE & Pgm()
The global program "get" accessor.
#define PROJECT_BACKUPS_DIR_SUFFIX
Project settings path will be <projectname> + this.
BACKUP_LOCATION location
Where backups, history, and autosave files live.
BACKUP_FORMAT format
Backup format (incremental git history vs zip archives)
Data produced by a registered saver on the UI thread, consumed by either the background local-history...
IbisParser parser & reporter
wxString result
Test unit parsing edge cases and error handling.
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
wxLogTrace helper definitions.
Definition of file extensions used in Kicad.