42#include <wx/filename.h>
46#include <wx/datetime.h>
70 const wxString& aRelativePath )
72 wxFileName fn( aRelativePath );
75 return fn.GetFullPath();
80 wxArrayString dirs = fn.GetDirs();
83 dst.AssignDir( aHistoryRoot );
85 for(
const wxString& d : dirs )
88 dst.SetFullName( fn.GetFullName() );
89 return dst.GetFullPath();
102 const wxString& aRelativePath,
105 wxFileName rel( aRelativePath );
107 dst.AssignDir( aAutosaveRoot );
109 for(
const wxString& d : rel.GetDirs() )
115 dst.SetFullName( rel.GetFullName() );
117 return dst.GetFullPath();
125 const wxString& aProjectPath,
126 const wxString& aAutosaveRoot,
129 wxFileName autosave( aAutosavePath );
133 wxString
name = autosave.GetFullName();
136 return wxEmptyString;
139 return autosave.GetFullPath();
142 if( !aAutosavePath.StartsWith( aAutosaveRoot ) )
143 return wxEmptyString;
145 wxString rel = aAutosavePath.Mid( aAutosaveRoot.length() );
146 wxFileName projFn( aProjectPath, wxEmptyString );
148 return projFn.GetPathWithSep() + rel;
153 const wxString& aTitle );
174 if( aProjectPath.IsEmpty() || !wxDirExists( aProjectPath ) )
177 wxDir dir( aProjectPath );
180 return dir.IsOpened()
191 return aName == wxS(
".history" )
192 || aName == wxS(
".git" )
193 || aName == wxS(
"_restore_backup" )
194 || aName.StartsWith( wxS(
"_restore_backup_" ) )
195 || aName == wxS(
"_restore_temp" )
210 wxFileName fn( aFile );
212 if( fn.GetFullName() == wxS(
"fp-info-cache" ) || !
Pgm().GetCommonSettings()->m_Backup.enabled )
220 const void* aSaverObject,
221 const std::function<
void(
const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
225 wxLogTrace(
traceAutoSave, wxS(
"[history] Saver %p already registered, skipping"), aSaverObject );
230 wxLogTrace(
traceAutoSave, wxS(
"[history] Registered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
238 auto it =
m_savers.find( aSaverObject );
243 wxLogTrace(
traceAutoSave, wxS(
"[history] Unregistered saver %p (total=%zu)"), aSaverObject,
m_savers.size() );
252 wxLogTrace(
traceAutoSave, wxS(
"[history] Cleared all savers") );
257 const wxString& aTagFileType )
259 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
261 wxLogTrace(
traceAutoSave, wxS(
"Autosave disabled, returning" ) );
267 wxLogTrace(
traceAutoSave, wxS(
"[history] Backup format is ZIP; skipping git commit" ) );
274 Init( aProjectPath );
277 wxS(
"[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu tag='%s'" ),
278 aProjectPath, aTitle,
m_savers.size(), aTagFileType );
282 wxLogTrace(
traceAutoSave, wxS(
"[history] no savers registered; skipping") );
287 if( !aTagFileType.IsEmpty() )
293 wxLogTrace(
traceAutoSave, wxS(
"[history] previous save still in progress; skipping cycle") );
298 std::vector<HISTORY_FILE_DATA> fileData;
300 for(
const auto& [saverObject, saver] :
m_savers )
302 size_t before = fileData.size();
303 saver( aProjectPath, fileData );
304 wxLogTrace(
traceAutoSave, wxS(
"[history] saver %p produced %zu entries (total=%zu)"),
305 saverObject, fileData.size() - before, fileData.size() );
311 auto it = std::remove_if( fileData.begin(), fileData.end(),
314 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
316 wxLogTrace( traceAutoSave, wxS(
"[history] filtered out entry with invalid path: '%s'"),
317 entry.relativePath );
322 fileData.erase( it, fileData.end() );
324 if( fileData.empty() )
326 wxLogTrace(
traceAutoSave, wxS(
"[history] saver set produced no entries; skipping") );
331 m_saveInProgress.store(
true, std::memory_order_release );
334 [
this, projectPath = aProjectPath, title = aTitle, tagFileType = aTagFileType,
335 data = std::move( fileData )]()
mutable ->
bool
337 bool result = commitInBackground( projectPath, title, data, !tagFileType.IsEmpty() );
339 if( !tagFileType.IsEmpty() )
340 TagSave( projectPath, tagFileType );
342 m_saveInProgress.store(
false, std::memory_order_release );
347 if( !aTagFileType.IsEmpty() )
348 WaitForPendingSave();
356 if( !
Pgm().GetCommonSettings()->m_Backup.enabled )
361 wxLogTrace(
traceAutoSave, wxS(
"[autosave] no savers registered; skipping") );
371 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create autosave root '%s'"), autosaveRoot );
375 std::vector<HISTORY_FILE_DATA> fileData;
377 for(
const auto& [saverObject, saver] :
m_savers )
378 saver( aProjectPath, fileData );
380 bool anyWritten =
false;
384 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
388 wxFileName dstFn( dst );
392 wxLogTrace(
traceAutoSave, wxS(
"[autosave] cannot create dir '%s'"), dstFn.GetPath() );
398 if( !entry.content.empty() )
400 buf = std::move( entry.content );
405 else if( !entry.sourcePath.IsEmpty() )
407 wxFFile src( entry.sourcePath, wxS(
"rb" ) );
409 if( !src.IsOpened() )
412 wxFileOffset len = src.Length();
417 buf.resize(
static_cast<size_t>( len ) );
419 if( len > 0 && src.Read( buf.data(), buf.size() ) != buf.size() )
435 wxLogTrace(
traceAutoSave, wxS(
"[autosave] wrote %zu bytes to '%s'"), buf.size(), dst );
439 wxLogTrace(
traceAutoSave, wxS(
"[autosave] write failed for '%s': %s"), dst, err );
452static std::vector<std::pair<wxString, wxString>>
455 std::vector<std::pair<wxString, wxString>> results;
461 if( !wxDirExists( autosaveRoot ) )
464 std::function<void(
const wxString& )> walk = [&](
const wxString& aDir )
472 bool cont = d.GetFirst( &
name );
476 wxFileName fn( aDir,
name );
477 wxString fullPath = fn.GetFullPath();
479 if( wxDirExists( fullPath ) )
482 && (
name == wxS(
".history" ) ||
name.EndsWith( wxS(
"-backups" ) ) ) )
484 cont = d.GetNext( &
name );
497 results.emplace_back( fullPath, src );
500 cont = d.GetNext( &
name );
504 walk( autosaveRoot );
509std::vector<std::pair<wxString, wxString>>
512 std::vector<std::pair<wxString, wxString>> results;
514 if( aExtensions.empty() )
519 wxFileName srcFn( pair.second );
522 for(
const wxString& ext : aExtensions )
524 if( srcFn.GetExt().IsSameAs( ext,
false ) )
536 if( srcFn.FileExists() )
537 srcTime = srcFn.GetModificationTime();
539 wxDateTime autosaveTime = wxFileName( pair.first ).GetModificationTime();
541 if( !srcTime.IsValid() || autosaveTime.IsLaterThan( srcTime ) )
542 results.emplace_back( std::move( pair ) );
556 if( wxFileExists( autosavePath ) )
557 wxRemoveFile( autosavePath );
563 const std::vector<wxString>& aSourcePaths )
const
565 if( aSourcePaths.empty() )
568 std::vector<wxFileName> targets;
569 targets.reserve( aSourcePaths.size() );
571 for(
const wxString& src : aSourcePaths )
574 targets.emplace_back( src );
577 if( targets.empty() )
582 wxFileName srcFn( srcPath );
585 for(
const wxFileName& target : targets )
587 if( srcFn.SameAs( target ) )
594 if( match && wxFileExists( autosavePath ) )
595 wxRemoveFile( autosavePath );
601 const std::vector<HISTORY_FILE_DATA>& aFileData,
bool aIsManualSave )
603 wxLogTrace(
traceAutoSave, wxS(
"[history] background: writing %zu entries for '%s'"),
604 aFileData.size(), aProjectPath );
610 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create history root '%s'"), hist );
617 wxFileName dstFn( dst );
618 wxString parent = dstFn.GetPath();
622 wxLogTrace(
traceAutoSave, wxS(
"[history] background: cannot create dir '%s'"), parent );
626 if( !entry.content.empty() )
628 std::string buf = entry.content;
633 wxFFile fp( dst, wxS(
"wb" ) );
637 fp.Write( buf.data(), buf.size() );
639 wxLogTrace(
traceAutoSave, wxS(
"[history] background: wrote %zu bytes to '%s'"),
644 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to open '%s' for writing"), dst );
647 else if( !entry.sourcePath.IsEmpty() )
649 wxCopyFile( entry.sourcePath, dst,
true );
650 wxLogTrace(
traceAutoSave, wxS(
"[history] background: copied '%s' -> '%s'"),
651 entry.sourcePath, dst );
667 git_repository_set_workdir( repo, hist.mb_str().data(),
false );
673 wxString rel = entry.relativePath;
674 rel.Replace( wxS(
"\\" ), wxS(
"/" ) );
678 if( !wxFileExists( abs ) )
681 git_index_add_bypath(
index, rel.ToStdString().c_str() );
686 git_commit* head_commit =
nullptr;
687 git_tree* head_tree =
nullptr;
689 bool headExists = ( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) == 0 )
690 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
691 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
693 git_tree* rawIndexTree =
nullptr;
694 git_oid index_tree_oid;
696 if( git_index_write_tree( &index_tree_oid,
index ) != 0 )
698 if( head_tree ) git_tree_free( head_tree );
699 if( head_commit ) git_commit_free( head_commit );
700 wxLogTrace(
traceAutoSave, wxS(
"[history] background: failed to write index tree" ) );
704 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
705 std::unique_ptr<git_tree,
decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
707 bool hasChanges =
true;
711 git_diff* diff =
nullptr;
713 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(),
nullptr ) == 0 )
715 hasChanges = git_diff_num_deltas( diff ) > 0;
716 wxLogTrace(
traceAutoSave, wxS(
"[history] background: diff deltas=%u"), (
unsigned) git_diff_num_deltas( diff ) );
717 git_diff_free( diff );
724 bool stagedMatchesDisk =
true;
728 wxString diskPath = aProjectPath + wxFileName::GetPathSeparator() + entry.relativePath;
731 if( !wxFileExists( diskPath ) || !wxFileExists( histPath ) )
733 stagedMatchesDisk =
false;
737 wxFFile diskFile( diskPath, wxT(
"rb" ) );
738 wxFFile histFile( histPath, wxT(
"rb" ) );
740 if( !diskFile.IsOpened() || !histFile.IsOpened() || diskFile.Length() != histFile.Length() )
742 stagedMatchesDisk =
false;
746 size_t len =
static_cast<size_t>( diskFile.Length() );
747 std::string diskBuf( len,
'\0' );
748 std::string histBuf( len,
'\0' );
750 if( diskFile.Read( diskBuf.data(), len ) != len || histFile.Read( histBuf.data(), len ) != len
751 || diskBuf != histBuf )
753 stagedMatchesDisk =
false;
758 if( stagedMatchesDisk && !aIsManualSave )
760 wxLogTrace(
traceAutoSave, wxS(
"[history] background: first commit; staged matches disk -- skipping" ) );
765 if( head_tree ) git_tree_free( head_tree );
766 if( head_commit ) git_commit_free( head_commit );
770 wxLogTrace(
traceAutoSave, wxS(
"[history] background: no changes detected; no commit") );
774 if( !aTitle.IsEmpty() && aTitle != wxS(
"Autosave" ) )
776 git_oid head_oid_amend;
778 if( git_reference_name_to_id( &head_oid_amend, repo,
"HEAD" ) == 0 )
780 git_commit* head_commit_amend =
nullptr;
782 if( git_commit_lookup( &head_commit_amend, repo, &head_oid_amend ) == 0 )
784 wxString existingMsg = wxString::FromUTF8( git_commit_message( head_commit_amend ) );
785 existingMsg.Trim(
true ).Trim(
false );
787 if( existingMsg != aTitle )
790 int amend_rc = git_commit_amend( &amended_oid, head_commit_amend,
"HEAD",
nullptr,
nullptr,
791 nullptr, aTitle.mb_str().data(),
nullptr );
794 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amended HEAD message '%s' -> '%s'" ),
795 existingMsg, aTitle );
797 wxLogTrace(
traceAutoSave, wxS(
"[history] background: amend failed rc=%d" ), amend_rc );
800 git_commit_free( head_commit_amend );
808 git_signature* rawSig =
nullptr;
810 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
812 git_commit* parent =
nullptr;
816 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
818 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
822 wxString msg = aTitle.IsEmpty() ? wxString(
"Autosave" ) : aTitle;
824 const git_commit* constParent = parent;
826 int rc = git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
827 msg.mb_str().data(), indexTree.get(), parents,
828 parents ? &constParent :
nullptr );
831 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit created %s (%s entries=%zu)"),
832 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
834 wxLogTrace(
traceAutoSave, wxS(
"[history] background: commit failed rc=%d"), rc );
836 if( parent ) git_commit_free( parent );
838 git_index_write(
index );
847 wxLogTrace(
traceAutoSave, wxS(
"[history] waiting for pending background save") );
871 if( !wxDirExists( hist ) )
880 git_repository* rawRepo =
nullptr;
882 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
884 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
887 wxFileName ignoreFile( hist, wxS(
".gitignore" ) );
888 if( !ignoreFile.FileExists() )
890 wxFFile f( ignoreFile.GetFullPath(), wxT(
"w" ) );
893 f.Write( wxS(
"# KiCad local history exclusions. Edit to add your own rules.\n"
900 wxFileName readmeFile( hist, wxS(
"README.txt" ) );
902 if( !readmeFile.FileExists() )
904 wxFFile f( readmeFile.GetFullPath(), wxT(
"w" ) );
908 f.Write( wxS(
"KiCad Local History Directory\n"
909 "=============================\n\n"
910 "This directory contains automatic snapshots of your project files.\n"
911 "KiCad periodically saves copies of your work here, allowing you to\n"
912 "recover from accidental changes or data loss.\n\n"
913 "You can browse and restore previous versions through KiCad's\n"
914 "File > Local History menu.\n\n"
915 "To disable this feature:\n"
916 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
917 "This directory can be safely deleted if you no longer need the\n"
918 "history, but doing so will permanently remove all saved snapshots.\n" ) );
924 git_repository_free( rawRepo );
940 const wxString& aHistoryPath,
const wxString& aProjectPath,
941 const std::vector<wxString>& aFiles,
const wxString& aTitle )
943 std::vector<std::string> filesArrStr;
945 for(
const wxString& file : aFiles )
947 wxFileName src( file );
950 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
951 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
953 relPath = src.GetFullName();
955 relPath.Replace(
"\\",
"/" );
956 std::string relPathStr = relPath.ToStdString();
958 unsigned int status = 0;
959 int rc = git_status_file( &status, repo, relPathStr.data() );
961 if( rc == 0 && status != 0 )
963 wxLogTrace(
traceAutoSave, wxS(
"File %s status %d " ), relPath, status );
964 filesArrStr.emplace_back( relPathStr );
968 wxLogTrace(
traceAutoSave, wxS(
"File %s status error %d " ), relPath, rc );
969 filesArrStr.emplace_back( relPathStr );
973 std::vector<char*> cStrings( filesArrStr.size() );
975 for(
size_t i = 0; i < filesArrStr.size(); i++ )
976 cStrings[i] = filesArrStr[i].data();
978 git_strarray filesArrGit;
979 filesArrGit.count = filesArrStr.size();
980 filesArrGit.strings = cStrings.data();
982 if( filesArrStr.size() == 0 )
988 int rc = git_index_add_all(
index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
990 wxLogTrace(
traceAutoSave, wxS(
"Adding %zu files, rc %d" ), filesArrStr.size(), rc );
996 if( git_index_write_tree( &tree_id,
index ) != 0 )
999 git_tree* rawTree =
nullptr;
1000 git_tree_lookup( &rawTree, repo, &tree_id );
1001 std::unique_ptr<git_tree,
decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
1003 git_signature* rawSig =
nullptr;
1005 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( rawSig,
1006 &git_signature_free );
1008 git_commit* rawParent =
nullptr;
1012 if( git_reference_name_to_id( &parent_id, repo,
"HEAD" ) == 0 )
1014 git_commit_lookup( &rawParent, repo, &parent_id );
1018 std::unique_ptr<git_commit,
decltype( &git_commit_free )> parent( rawParent,
1021 git_tree* rawParentTree =
nullptr;
1024 git_commit_tree( &rawParentTree, parent.get() );
1026 std::unique_ptr<git_tree,
decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
1028 git_diff* rawDiff =
nullptr;
1029 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(),
index,
nullptr );
1030 std::unique_ptr<git_diff,
decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
1032 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
1034 if( numChangedFiles == 0 )
1036 wxLogTrace(
traceAutoSave, wxS(
"No actual changes in tree, skipping commit" ) );
1042 if( !aTitle.IsEmpty() )
1043 msg << aTitle << wxS(
": " );
1045 msg << numChangedFiles << wxS(
" files changed" );
1047 for(
size_t i = 0; i < numChangedFiles; ++i )
1049 const git_diff_delta*
delta = git_diff_get_delta( diff.get(), i );
1050 git_patch* rawPatch =
nullptr;
1051 git_patch_from_diff( &rawPatch, diff.get(), i );
1052 std::unique_ptr<git_patch,
decltype( &git_patch_free )> patch( rawPatch,
1054 size_t context = 0, adds = 0, dels = 0;
1055 git_patch_line_stats( &context, &adds, &dels, patch.get() );
1056 size_t updated = std::min( adds, dels );
1059 msg << wxS(
"\n" ) << wxString::FromUTF8(
delta->new_file.path )
1060 << wxS(
" " ) << adds << wxS(
"/" ) << dels << wxS(
"/" ) << updated;
1064 git_commit* parentPtr = parent.get();
1065 const git_commit* constParentPtr = parentPtr;
1066 if( git_commit_create( &commit_id, repo,
"HEAD", sig.get(), sig.get(),
nullptr, msg.mb_str().data(), tree.get(),
1067 parents, parentPtr ? &constParentPtr :
nullptr )
1073 git_index_write(
index );
1082 const wxString& aTitle )
1090 wxLogTrace(
traceAutoSave, wxS(
"[history] commitSnapshotForProject failed to acquire lock: %s" ),
1102 if( aFiles.empty() || !
Pgm().GetCommonSettings()->m_Backup.enabled
1108 wxString proj = wxFileName( aFiles[0] ).GetPath();
1122 wxString
name = aFile.GetFullName();
1124 if(
name == wxS(
"sym-lib-table" ) ||
name == wxS(
"fp-lib-table" ) )
1127 return aFile.GetExt().StartsWith( wxS(
"kicad_" ) );
1136 wxDir dir( aProjectPath );
1138 if( !dir.IsOpened() )
1142 std::function<void(
const wxString&,
bool )> collect =
1143 [&](
const wxString&
path,
bool topLevel )
1148 wxS(
"[history] collectProjectFiles: Skipping nested project at %s" ),
1159 bool cont = d.GetFirst( &
name );
1165 cont = d.GetNext( &
name );
1170 wxString fullPath = fn.GetFullPath();
1172 if( wxFileName::DirExists( fullPath ) )
1174 collect( fullPath,
false );
1176 else if( fn.FileExists() && fn.GetFullName() != wxS(
"fp-info-cache" ) &&
isKiCadProjectFile( fn ) )
1178 aFiles.push_back( fn.GetFullPath() );
1181 cont = d.GetNext( &
name );
1185 collect( aProjectPath,
true );
1196 wxLogTrace(
traceAutoSave, wxS(
"[history] Backup format is ZIP; skipping full snapshot" ) );
1200 std::vector<wxString> files;
1206 Init( aProjectPath );
1212 return wxDirExists(
historyPath( aProjectPath ) );
1227 wxLogTrace(
traceAutoSave, wxS(
"[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
1237 if( git_reference_name_to_id( &head, repo,
"HEAD" ) != 0 )
1242 git_reference* ref =
nullptr;
1245 tagName.Printf( wxS(
"Save_%s_%d" ), aFileType, i++ );
1246 }
while( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
1249 git_object* head_obj =
nullptr;
1250 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
1251 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
1252 git_object_free( head_obj );
1255 lastName.Printf( wxS(
"Last_Save_%s" ), aFileType );
1256 if( git_reference_lookup( &ref, repo, ( wxS(
"refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
1258 git_reference_delete( ref );
1259 git_reference_free( ref );
1262 git_oid last_tag_oid;
1263 git_object* head_obj2 =
nullptr;
1264 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
1265 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
1266 git_object_free( head_obj2 );
1274 git_repository* repo =
nullptr;
1276 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1280 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1282 git_repository_free( repo );
1286 git_commit* head_commit =
nullptr;
1287 git_commit_lookup( &head_commit, repo, &head_oid );
1288 git_time_t head_time = git_commit_time( head_commit );
1291 git_tag_list_match( &tags,
"Last_Save_*", repo );
1292 git_time_t save_time = 0;
1294 for(
size_t i = 0; i < tags.count; ++i )
1296 git_reference* ref =
nullptr;
1297 if( git_reference_lookup( &ref, repo,
1298 ( wxS(
"refs/tags/" ) +
1299 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
1301 const git_oid* oid = git_reference_target( ref );
1302 git_commit* c =
nullptr;
1303 if( git_commit_lookup( &c, repo, oid ) == 0 )
1305 git_time_t t = git_commit_time( c );
1308 git_commit_free( c );
1310 git_reference_free( ref );
1314 git_strarray_free( &tags );
1315 git_commit_free( head_commit );
1316 git_repository_free( repo );
1320 if( save_time == 0 )
1323 return head_time > save_time;
1327 const wxString& aMessage )
1339 wxLogTrace(
traceAutoSave, wxS(
"[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
1348 wxString lastName; lastName.Printf( wxS(
"Last_Save_%s"), aFileType );
1349 git_reference* lastRef =
nullptr;
1350 if( git_reference_lookup( &lastRef, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) != 0 )
1352 std::unique_ptr<git_reference,
decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
1354 const git_oid* lastOid = git_reference_target( lastRef );
1355 git_commit* lastCommit =
nullptr;
1356 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
1358 std::unique_ptr<git_commit,
decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
1360 git_tree* lastTree =
nullptr;
1361 git_commit_tree( &lastTree, lastCommit );
1362 std::unique_ptr<git_tree,
decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
1366 git_commit* headCommit =
nullptr;
1368 const git_commit* parentArray[1];
1369 if( git_reference_name_to_id( &headOid, repo,
"HEAD" ) == 0 &&
1370 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
1372 parentArray[0] = headCommit;
1376 git_signature* sigRaw =
nullptr;
1378 std::unique_ptr<git_signature,
decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
1380 wxString msg = aMessage.IsEmpty() ? wxS(
"Discard unsaved ") + aFileType : aMessage;
1381 git_oid newCommitOid;
1382 int rc = git_commit_create( &newCommitOid, repo,
"HEAD", sig.get(), sig.get(),
nullptr,
1383 msg.mb_str().data(), lastTree, parents, parents ? parentArray :
nullptr );
1384 if( headCommit ) git_commit_free( headCommit );
1389 git_reference* existing =
nullptr;
1390 if( git_reference_lookup( &existing, repo, ( wxS(
"refs/tags/") + lastName ).mb_str().data() ) == 0 )
1392 git_reference_delete( existing );
1393 git_reference_free( existing );
1395 git_object* newCommitObj =
nullptr;
1396 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
1398 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
1399 git_object_free( newCommitObj );
1408 if( !dir.IsOpened() )
1411 bool cont = dir.GetFirst( &
name );
1415 wxString fullPath = fn.GetFullPath();
1417 if( wxFileName::DirExists( fullPath ) )
1419 else if( fn.FileExists() )
1420 total += (size_t) fn.GetSize().GetValue();
1421 cont = dir.GetNext( &
name );
1427static bool copyTreeObjects( git_repository* aSrcRepo, git_odb* aSrcOdb, git_odb* aDstOdb,
const git_oid* aTreeOid,
1428 std::set<git_oid,
bool ( * )(
const git_oid&,
const git_oid& )>& aCopied )
1430 if( aCopied.count( *aTreeOid ) )
1433 git_odb_object* obj =
nullptr;
1435 if( git_odb_read( &obj, aSrcOdb, aTreeOid ) != 0 )
1439 int err = git_odb_write( &written, aDstOdb, git_odb_object_data( obj ), git_odb_object_size( obj ),
1440 git_odb_object_type( obj ) );
1441 git_odb_object_free( obj );
1446 aCopied.insert( *aTreeOid );
1448 git_tree* tree =
nullptr;
1450 if( git_tree_lookup( &tree, aSrcRepo, aTreeOid ) != 0 )
1453 size_t cnt = git_tree_entrycount( tree );
1455 for(
size_t i = 0; i < cnt; ++i )
1457 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1458 const git_oid* entryId = git_tree_entry_id( entry );
1460 if( aCopied.count( *entryId ) )
1463 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1465 if( !
copyTreeObjects( aSrcRepo, aSrcOdb, aDstOdb, entryId, aCopied ) )
1467 git_tree_free( tree );
1471 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1473 git_odb_object* blobObj =
nullptr;
1475 if( git_odb_read( &blobObj, aSrcOdb, entryId ) == 0 )
1477 git_oid blobWritten;
1479 if( git_odb_write( &blobWritten, aDstOdb, git_odb_object_data( blobObj ),
1480 git_odb_object_size( blobObj ), git_odb_object_type( blobObj ) )
1483 git_odb_object_free( blobObj );
1484 git_tree_free( tree );
1488 git_odb_object_free( blobObj );
1489 aCopied.insert( *entryId );
1494 git_tree_free( tree );
1503 git_packbuilder* pb =
nullptr;
1505 if( git_packbuilder_new( &pb, aRepo ) != 0 )
1508 git_revwalk* walk =
nullptr;
1510 if( git_revwalk_new( &walk, aRepo ) != 0 )
1512 git_packbuilder_free( pb );
1516 git_revwalk_push_head( walk );
1519 while( git_revwalk_next( &oid, walk ) == 0 )
1521 if( git_packbuilder_insert_commit( pb, &oid ) != 0 )
1523 git_revwalk_free( walk );
1524 git_packbuilder_free( pb );
1529 git_revwalk_free( walk );
1533 git_packbuilder_set_callbacks(
1535 [](
int aStage, uint32_t aCurrent, uint32_t aTotal,
void* aPayload )
1542 reporter->KeepRefreshing();
1548 if( git_packbuilder_write( pb,
nullptr, 0,
nullptr,
nullptr ) != 0 )
1550 git_packbuilder_free( pb );
1554 git_packbuilder_free( pb );
1556 wxString objPath = wxString::FromUTF8( git_repository_path( aRepo ) ) + wxS(
"objects" );
1557 wxDir objDir( objPath );
1559 if( objDir.IsOpened() )
1561 wxArrayString toRemove;
1563 bool cont = objDir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS );
1567 if(
name.length() == 2 )
1568 toRemove.Add( objPath + wxFileName::GetPathSeparator() +
name );
1570 cont = objDir.GetNext( &
name );
1573 for(
const wxString& dir : toRemove )
1574 wxFileName::Rmdir( dir, wxPATH_RMDIR_RECURSIVE );
1583 if( aMaxBytes == 0 )
1588 if( !wxDirExists( hist ) )
1593 if( current <= aMaxBytes )
1600 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
1610 aReporter->
Report(
_(
"Compacting local history..." ) );
1617 if( current <= aMaxBytes )
1621 git_revwalk* walk =
nullptr;
1622 git_revwalk_new( &walk, repo );
1623 git_revwalk_sorting( walk, GIT_SORT_TIME );
1624 git_revwalk_push_head( walk );
1625 std::vector<git_oid> commits;
1628 while( git_revwalk_next( &oid, walk ) == 0 )
1629 commits.push_back( oid );
1631 git_revwalk_free( walk );
1633 if( commits.empty() )
1637 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> seenBlobs(
1638 [](
const git_oid& a,
const git_oid& b )
1640 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1643 size_t keptBytes = 0;
1644 std::vector<git_oid> keep;
1646 git_odb* odb =
nullptr;
1647 git_repository_odb( &odb, repo );
1649 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
1652 size_t cnt = git_tree_entrycount( tree );
1654 for(
size_t i = 0; i < cnt; ++i )
1656 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1658 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1660 const git_oid* bid = git_tree_entry_id( entry );
1662 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1665 git_object_t type = GIT_OBJECT_ANY;
1667 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1670 seenBlobs.insert( *bid );
1673 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1675 git_tree* sub =
nullptr;
1677 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1679 added += accountTree( sub );
1680 git_tree_free( sub );
1688 for(
const git_oid& cOid : commits )
1690 git_commit* c =
nullptr;
1692 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1695 git_tree* tree =
nullptr;
1696 git_commit_tree( &tree, c );
1697 size_t add = accountTree( tree );
1698 git_tree_free( tree );
1699 git_commit_free( c );
1701 if( keep.empty() || keptBytes + add <= aMaxBytes )
1703 keep.push_back( cOid );
1711 keep.push_back( commits.front() );
1715 std::vector<std::pair<wxString, git_oid>> tagTargets;
1716 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> taggedCommits(
1717 [](
const git_oid& a,
const git_oid& b )
1719 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1721 git_strarray tagList;
1723 if( git_tag_list( &tagList, repo ) == 0 )
1725 for(
size_t i = 0; i < tagList.count; ++i )
1727 wxString
name = wxString::FromUTF8( tagList.strings[i] );
1728 if(
name.StartsWith( wxS(
"Save_") ) ||
name.StartsWith( wxS(
"Last_Save_") ) )
1730 git_reference* tref =
nullptr;
1732 if( git_reference_lookup( &tref, repo, ( wxS(
"refs/tags/" ) +
name ).mb_str().data() ) == 0 )
1734 const git_oid* toid = git_reference_target( tref );
1738 tagTargets.emplace_back(
name, *toid );
1739 taggedCommits.insert( *toid );
1743 for(
const auto& k : keep )
1745 if( memcmp( &k, toid,
sizeof( git_oid ) ) == 0 )
1755 keep.push_back( *toid );
1756 wxLogTrace(
traceAutoSave, wxS(
"[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1761 git_reference_free( tref );
1765 git_strarray_free( &tagList );
1769 wxFileName trimFn( hist + wxS(
"_trim"), wxEmptyString );
1770 wxString trimPath = trimFn.GetPath();
1772 if( wxDirExists( trimPath ) )
1773 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1775 wxMkdir( trimPath );
1776 git_repository* newRepo =
nullptr;
1778 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1780 git_odb_free( odb );
1784 git_odb* dstOdb =
nullptr;
1786 if( git_repository_odb( &dstOdb, newRepo ) != 0 )
1788 git_repository_free( newRepo );
1789 git_odb_free( odb );
1793 std::set<git_oid, bool ( * )(
const git_oid&,
const git_oid& )> copiedObjects(
1794 [](
const git_oid& a,
const git_oid& b )
1796 return memcmp( &a, &b,
sizeof( git_oid ) ) < 0;
1800 std::reverse( keep.begin(), keep.end() );
1801 git_commit* parent =
nullptr;
1802 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1803 std::vector<MAP_ENTRY> commitMap;
1807 aReporter->
AdvancePhase(
_(
"Trimming local history..." ) );
1811 for(
size_t idx = 0; idx < keep.size(); ++idx )
1816 const git_oid& co = keep[idx];
1817 git_commit* orig =
nullptr;
1819 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1822 git_tree* tree =
nullptr;
1823 git_commit_tree( &tree, orig );
1825 copyTreeObjects( repo, odb, dstOdb, git_tree_id( tree ), copiedObjects );
1827 git_tree* newTree =
nullptr;
1828 git_tree_lookup( &newTree, newRepo, git_tree_id( tree ) );
1830 git_tree_free( tree );
1833 const git_signature* origAuthor = git_commit_author( orig );
1834 const git_signature* origCommitter = git_commit_committer( orig );
1835 git_signature* sigAuthor =
nullptr;
1836 git_signature* sigCommitter =
nullptr;
1838 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1839 origAuthor->when.time, origAuthor->when.offset );
1840 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1841 origCommitter->when.time, origCommitter->when.offset );
1843 const git_commit* parents[1];
1844 int parentCount = 0;
1848 parents[0] = parent;
1852 git_oid newCommitOid;
1853 git_commit_create( &newCommitOid, newRepo,
"HEAD", sigAuthor, sigCommitter,
nullptr, git_commit_message( orig ),
1854 newTree, parentCount, parentCount ? parents :
nullptr );
1857 git_commit_free( parent );
1859 git_commit_lookup( &parent, newRepo, &newCommitOid );
1861 commitMap.emplace_back( co, newCommitOid );
1863 git_signature_free( sigAuthor );
1864 git_signature_free( sigCommitter );
1865 git_tree_free( newTree );
1866 git_commit_free( orig );
1870 git_commit_free( parent );
1873 for(
const auto& tt : tagTargets )
1876 const git_oid* newOid =
nullptr;
1878 for(
const auto& m : commitMap )
1880 if( memcmp( &m.orig, &tt.second,
sizeof( git_oid ) ) == 0 )
1890 git_object* obj =
nullptr;
1892 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1894 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1895 git_object_free( obj );
1900 aReporter->
AdvancePhase(
_(
"Compacting trimmed history..." ) );
1907 git_odb_free( dstOdb );
1908 git_odb_free( odb );
1909 git_repository_free( newRepo );
1914 wxString backupOld = hist + wxS(
"_old");
1915 wxRenameFile( hist, backupOld );
1916 wxRenameFile( trimPath, hist );
1917 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1924 git_repository* repo =
nullptr;
1926 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1927 return wxEmptyString;
1930 if( git_reference_name_to_id( &head_oid, repo,
"HEAD" ) != 0 )
1932 git_repository_free( repo );
1933 return wxEmptyString;
1936 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1937 git_repository_free( repo );
1949bool checkForLockedFiles(
const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1951 std::function<void(
const wxString& )> findLocks = [&](
const wxString& dirPath )
1953 wxDir dir( dirPath );
1954 if( !dir.IsOpened() )
1958 bool cont = dir.GetFirst( &filename );
1962 wxFileName fullPath( dirPath, filename );
1965 if( filename == wxS(
".history") || filename == wxS(
".git") )
1967 cont = dir.GetNext( &filename );
1971 if( fullPath.DirExists() )
1973 findLocks( fullPath.GetFullPath() );
1975 else if( fullPath.FileExists()
1982 baseName = baseName.BeforeLast(
'.' );
1983 wxFileName originalFile( dirPath, baseName );
1986 LOCKFILE testLock( originalFile.GetFullPath() );
1987 if( testLock.Valid() && !testLock.IsLockedByMe() )
1989 aLockedFiles.push_back( fullPath.GetFullPath() );
1993 cont = dir.GetNext( &filename );
1997 findLocks( aProjectPath );
1998 return aLockedFiles.empty();
2005bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree,
const wxString& aTempPath )
2007 bool extractSuccess =
true;
2009 std::function<void( git_tree*,
const wxString& )> extractTree =
2010 [&]( git_tree* t,
const wxString& prefix )
2012 if( !extractSuccess )
2015 size_t cnt = git_tree_entrycount( t );
2016 for(
size_t i = 0; i < cnt; ++i )
2018 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
2019 wxString
name = wxString::FromUTF8( git_tree_entry_name( entry ) );
2020 wxString fullPath = prefix.IsEmpty() ?
name : prefix + wxS(
"/") +
name;
2022 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
2024 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
2026 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
2029 wxS(
"[history] extractCommitToTemp: Failed to create directory '%s'" ),
2030 dirPath.GetPath() );
2031 extractSuccess =
false;
2035 git_tree* sub =
nullptr;
2036 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
2038 extractTree( sub, fullPath );
2039 git_tree_free( sub );
2042 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
2044 git_blob* blob =
nullptr;
2045 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
2047 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
2049 wxFileName dstDir( dst );
2050 dstDir.SetFullName( wxEmptyString );
2051 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
2053 wxFFile f( dst.GetFullPath(), wxT(
"wb" ) );
2056 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
2062 wxS(
"[history] extractCommitToTemp: Failed to write '%s'" ),
2063 dst.GetFullPath() );
2064 extractSuccess =
false;
2065 git_blob_free( blob );
2069 git_blob_free( blob );
2075 extractTree( aTree, wxEmptyString );
2076 return extractSuccess;
2083void collectFilesInDirectory(
const wxString& aRootPath,
const wxString& aSearchPath,
2084 std::set<wxString>& aFiles )
2086 wxDir dir( aSearchPath );
2087 if( !dir.IsOpened() )
2091 bool cont = dir.GetFirst( &filename );
2095 wxFileName fullPath( aSearchPath, filename );
2096 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
2098 if( fullPath.IsDir() && fullPath.DirExists() )
2100 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
2102 else if( fullPath.FileExists() )
2104 aFiles.insert( relativePath );
2107 cont = dir.GetNext( &filename );
2115bool shouldExcludeFromBackup(
const wxString& aFilename )
2122bool isPathUnderNestedProject(
const wxString& aProjectPath,
const wxString& aRelativePath )
2124 if( aRelativePath.IsEmpty() )
2127 wxArrayString parts = wxSplit( aRelativePath,
'/',
'\0' );
2129 if( parts.GetCount() < 2 )
2132 wxString accumulated = aProjectPath;
2136 for(
size_t i = 0; i + 1 < parts.GetCount(); ++i )
2138 accumulated += wxFileName::GetPathSeparator() + parts[i];
2151void findFilesToDelete(
const wxString& aProjectPath,
const std::set<wxString>& aRestoredFiles,
2152 std::vector<wxString>& aFilesToDelete )
2154 std::function<void(
const wxString&,
const wxString& )> scanDirectory =
2155 [&](
const wxString& dirPath,
const wxString& relativeBase )
2157 wxDir dir( dirPath );
2158 if( !dir.IsOpened() )
2162 bool cont = dir.GetFirst( &filename );
2170 cont = dir.GetNext( &filename );
2174 wxFileName fullPath( dirPath, filename );
2175 wxString relativePath = relativeBase.IsEmpty() ? filename :
2176 relativeBase + wxS(
"/") + filename;
2178 if( fullPath.IsDir() && fullPath.DirExists() )
2185 wxS(
"[history] findFilesToDelete: Skipping nested project "
2187 fullPath.GetFullPath() );
2191 scanDirectory( fullPath.GetFullPath(), relativePath );
2194 else if( fullPath.FileExists() )
2197 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
2200 if( !shouldExcludeFromBackup( filename ) )
2201 aFilesToDelete.push_back( relativePath );
2205 cont = dir.GetNext( &filename );
2209 scanDirectory( aProjectPath, wxEmptyString );
2217bool confirmFileDeletion( wxWindow* aParent,
const wxString& aProjectPath,
2218 const wxString& aBackupPath,
2219 const std::vector<wxString>& aFilesToDelete,
bool& aKeepAllFiles )
2221 if( aFilesToDelete.empty() || !aParent )
2223 aKeepAllFiles =
true;
2227 bool hasNestedProjectFile =
false;
2229 for(
const wxString& rel : aFilesToDelete )
2231 if( isPathUnderNestedProject( aProjectPath, rel ) )
2233 hasNestedProjectFile =
true;
2238 if( hasNestedProjectFile )
2240 wxLogTrace(
traceAutoSave, wxS(
"[history] Forcing keepAllFiles due to nested project under "
2241 "candidate path" ) );
2242 aKeepAllFiles =
true;
2246 wxString message =
_(
"The following files will be deleted when restoring this commit:\n\n" );
2249 size_t displayCount = std::min( aFilesToDelete.size(),
size_t(20) );
2250 for(
size_t i = 0; i < displayCount; ++i )
2252 message += wxS(
" • ") + aFilesToDelete[i] + wxS(
"\n");
2255 if( aFilesToDelete.size() > displayCount )
2257 message += wxString::Format(
_(
"\n... and %zu more files\n" ),
2258 aFilesToDelete.size() - displayCount );
2262 wxYES_NO | wxCANCEL | wxNO_DEFAULT | wxICON_QUESTION );
2263 dlg.SetYesNoCancelLabels(
_(
"Proceed" ),
_(
"Keep All Files" ),
_(
"Abort" ) );
2264 dlg.SetExtendedMessage(
2265 _(
"Choosing 'Keep All Files' will restore the selected commit but retain any existing "
2266 "files in the project directory. Choosing 'Proceed' will delete files that are not "
2267 "present in the restored commit." )
2269 + wxString::Format(
_(
"Files removed by 'Proceed' are archived to %s and can be "
2270 "recovered manually." ),
2273 int choice = dlg.ShowModal();
2275 if( choice == wxID_CANCEL )
2277 wxLogTrace(
traceAutoSave, wxS(
"[history] User cancelled restore" ) );
2280 else if( choice == wxID_NO )
2282 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to keep all files" ) );
2283 aKeepAllFiles =
true;
2287 wxLogTrace(
traceAutoSave, wxS(
"[history] User chose to proceed with deletion" ) );
2288 aKeepAllFiles =
false;
2298bool backupCurrentFiles(
const wxString& aProjectPath,
const wxString& aBackupPath,
2299 const wxString& aTempRestorePath,
bool aKeepAllFiles,
2300 std::set<wxString>& aBackedUpFiles )
2302 wxDir currentDir( aProjectPath );
2303 if( !currentDir.IsOpened() )
2307 bool cont = currentDir.GetFirst( &filename );
2316 bool shouldBackup = !aKeepAllFiles;
2321 wxFileName testPath( aTempRestorePath, filename );
2322 shouldBackup = testPath.Exists();
2327 wxFileName source( aProjectPath, filename );
2328 wxFileName dest( aBackupPath, filename );
2331 if( !wxDirExists( aBackupPath ) )
2334 wxS(
"[history] backupCurrentFiles: Creating backup directory %s" ),
2336 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
2340 wxS(
"[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
2341 source.GetFullPath(), dest.GetFullPath() );
2343 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
2346 wxS(
"[history] backupCurrentFiles: Failed to backup '%s'" ),
2347 source.GetFullPath() );
2351 aBackedUpFiles.insert( filename );
2354 cont = currentDir.GetNext( &filename );
2364bool restoreFilesFromTemp(
const wxString& aTempRestorePath,
const wxString& aProjectPath,
2365 std::set<wxString>& aRestoredFiles )
2367 wxDir tempDir( aTempRestorePath );
2368 if( !tempDir.IsOpened() )
2372 bool cont = tempDir.GetFirst( &filename );
2376 wxFileName source( aTempRestorePath, filename );
2377 wxFileName dest( aProjectPath, filename );
2380 wxS(
"[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
2381 source.GetFullPath(), dest.GetFullPath() );
2383 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
2386 wxS(
"[history] restoreFilesFromTemp: Failed to move '%s'" ),
2387 source.GetFullPath() );
2391 aRestoredFiles.insert( filename );
2392 cont = tempDir.GetNext( &filename );
2402void rollbackRestore(
const wxString& aProjectPath,
const wxString& aBackupPath,
2403 const wxString& aTempRestorePath,
const std::set<wxString>& aBackedUpFiles,
2404 const std::set<wxString>& aRestoredFiles )
2406 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Rolling back due to failure" ) );
2410 for(
const wxString& filename : aRestoredFiles )
2412 wxFileName toRemove( aProjectPath, filename );
2413 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Removing '%s'" ),
2414 toRemove.GetFullPath() );
2416 if( toRemove.DirExists() )
2418 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
2420 else if( toRemove.FileExists() )
2422 wxRemoveFile( toRemove.GetFullPath() );
2427 if( wxDirExists( aBackupPath ) )
2429 for(
const wxString& filename : aBackedUpFiles )
2431 wxFileName source( aBackupPath, filename );
2432 wxFileName dest( aProjectPath, filename );
2434 if( source.Exists() )
2436 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
2437 wxLogTrace(
traceAutoSave, wxS(
"[history] rollbackRestore: Restored '%s'" ),
2438 dest.GetFullPath() );
2444 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
2445 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
2452bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
2453 const wxString& aHash )
2455 git_time_t t = git_commit_time( aCommit );
2456 wxDateTime dt( (time_t) t );
2457 git_signature* sig =
nullptr;
2459 git_commit* parent =
nullptr;
2462 if( git_reference_name_to_id( &parent_id, aRepo,
"HEAD" ) == 0 )
2463 git_commit_lookup( &parent, aRepo, &parent_id );
2466 msg.Printf( wxS(
"Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
2469 const git_commit* constParent = parent;
2470 int result = git_commit_create( &new_id, aRepo,
"HEAD", sig, sig,
nullptr,
2471 msg.mb_str().data(), aTree, parent ? 1 : 0,
2472 parent ? &constParent :
nullptr );
2475 git_commit_free( parent );
2476 git_signature_free( sig );
2488 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Checking for open files in %s" ),
2491 std::vector<wxString> lockedFiles;
2492 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
2495 for(
const auto& f : lockedFiles )
2496 lockList += wxS(
"\n - ") + f;
2499 wxS(
"[history] RestoreCommit: Cannot restore - files are open:%s" ),
2505 wxString msg =
_(
"Cannot restore - the following files are open by another user:" );
2507 wxMessageBox( msg,
_(
"Restore Failed" ), wxOK | wxICON_WARNING, aParent );
2518 wxS(
"[history] RestoreCommit: Failed to acquire lock for %s" ),
2529 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
2531 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Invalid hash %s" ), aHash );
2535 git_commit* commit =
nullptr;
2536 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2538 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Commit not found %s" ), aHash );
2542 git_tree* tree =
nullptr;
2543 git_commit_tree( &tree, commit );
2546 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Creating pre-restore backup" ) );
2548 std::vector<wxString> backupFiles;
2551 if( !backupFiles.empty() )
2555 backupFiles, wxS(
"Pre-restore backup" ) );
2560 wxS(
"[history] RestoreCommit: Failed to create pre-restore backup" ) );
2561 git_tree_free( tree );
2562 git_commit_free( commit );
2568 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Current state already matches HEAD; "
2569 "continuing without a new backup commit" ) );
2574 wxString tempRestorePath = aProjectPath + wxS(
"_restore_temp");
2576 if( wxDirExists( tempRestorePath ) )
2577 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2579 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
2582 wxS(
"[history] RestoreCommit: Failed to create temp directory %s" ),
2584 git_tree_free( tree );
2585 git_commit_free( commit );
2589 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extracting to temp location %s" ),
2592 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
2594 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Extraction failed, cleaning up" ) );
2595 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2596 git_tree_free( tree );
2597 git_commit_free( commit );
2602 std::set<wxString> restoredFiles;
2603 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
2605 std::vector<wxString> filesToDelete;
2606 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
2613 wxString backupPath =
2614 aProjectPath + wxS(
"_restore_backup_" )
2615 + wxDateTime::UNow().Format( wxS(
"%Y-%m-%dT%H-%M-%S-%l" ) );
2617 bool keepAllFiles =
true;
2618 if( !confirmFileDeletion( aParent, aProjectPath, backupPath, filesToDelete, keepAllFiles ) )
2621 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2622 git_tree_free( tree );
2623 git_commit_free( commit );
2628 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Performing atomic swap" ) );
2631 std::set<wxString> backedUpFiles;
2632 std::set<wxString> restoredFilesSet;
2635 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
2638 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2640 git_tree_free( tree );
2641 git_commit_free( commit );
2646 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
2648 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
2650 git_tree_free( tree );
2651 git_commit_free( commit );
2657 wxS(
"[history] RestoreCommit: Restore successful, backup retained at %s" ),
2659 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2662 recordRestoreInHistory( repo, commit, tree, aHash );
2664 git_tree_free( tree );
2665 git_commit_free( commit );
2667 wxLogTrace(
traceAutoSave, wxS(
"[history] RestoreCommit: Complete" ) );
2676 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots =
LoadSnapshots( aProjectPath );
2678 if( snapshots.empty() )
2687 if( !selectedHash.IsEmpty() )
2694 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2697 git_repository* repo =
nullptr;
2699 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2702 git_revwalk* walk =
nullptr;
2703 if( git_revwalk_new( &walk, repo ) != 0 )
2705 git_repository_free( repo );
2709 git_revwalk_sorting( walk, GIT_SORT_TIME );
2710 git_revwalk_push_head( walk );
2714 while( git_revwalk_next( &oid, walk ) == 0 )
2716 git_commit* commit =
nullptr;
2718 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2722 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2723 info.date = wxDateTime(
static_cast<time_t
>( git_commit_time( commit ) ) );
2724 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2726 wxString firstLine =
info.message.BeforeFirst(
'\n' );
2728 long parsedCount = 0;
2730 firstLine.BeforeFirst(
':', &remainder );
2731 remainder.Trim(
true ).Trim(
false );
2733 if( remainder.EndsWith( wxS(
"files changed" ) ) )
2735 wxString countText = remainder.BeforeFirst(
' ' );
2737 if( countText.ToLong( &parsedCount ) )
2738 info.filesChanged =
static_cast<int>( parsedCount );
2741 info.summary = firstLine.BeforeFirst(
':' );
2744 info.message.BeforeFirst(
'\n', &rest );
2745 wxArrayString lines = wxSplit( rest,
'\n',
'\0' );
2747 for(
const wxString& line : lines )
2749 if( !line.IsEmpty() )
2750 info.changedFiles.Add( line );
2753 snapshots.push_back( std::move(
info ) );
2754 git_commit_free( commit );
2757 git_revwalk_free( walk );
2758 git_repository_free( repo );
wxString GetSelectedHash() const
Hybrid locking mechanism for local history git repositories.
git_repository * GetRepository()
Get the git repository handle (only valid if IsLocked() returns true).
void ReleaseRepository()
Release git repository and index handles early, but keep the file lock.
wxString GetLockError() const
Get error message describing why lock could not be acquired.
git_index * GetIndex()
Get the git index handle (only valid if IsLocked() returns true).
bool IsLocked() const
Check if locks were successfully acquired.
std::vector< LOCAL_HISTORY_SNAPSHOT_INFO > LoadSnapshots(const wxString &aProjectPath)
bool EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes, PROGRESS_REPORTER *aReporter=nullptr)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
bool TagSave(const wxString &aProjectPath, const wxString &aFileType)
Tag a manual save in the local history repository.
bool RunRegisteredSaversAndCommit(const wxString &aProjectPath, const wxString &aTitle, const wxString &aTagFileType=wxEmptyString)
Run all registered savers and, if any staged changes differ from HEAD, create a commit.
std::vector< std::pair< wxString, wxString > > FindStaleAutosaveFiles(const wxString &aProjectPath, const std::vector< wxString > &aExtensions) const
Enumerate autosave files newer than their corresponding source files for the project at aProjectPath,...
wxString GetHeadHash(const wxString &aProjectPath)
Return the current head commit hash.
bool RestoreCommit(const wxString &aProjectPath, const wxString &aHash, wxWindow *aParent=nullptr)
Restore the project files to the state recorded by the given commit hash.
bool commitInBackground(const wxString &aProjectPath, const wxString &aTitle, const std::vector< HISTORY_FILE_DATA > &aFileData, bool aIsManualSave)
Execute file writes and git commit on a background thread.
void ShowRestoreDialog(const wxString &aProjectPath, wxWindow *aParent)
Show a dialog allowing the user to choose a snapshot to restore.
bool HeadNewerThanLastSave(const wxString &aProjectPath)
Return true if the autosave data is newer than the last manual save.
std::set< wxString > m_pendingFiles
std::map< const void *, std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> > m_savers
bool CommitDuplicateOfLastSave(const wxString &aProjectPath, const wxString &aFileType, const wxString &aMessage)
Create a new commit duplicating the tree pointed to by Last_Save_<fileType> and move the Last_Save_<f...
void WaitForPendingSave()
Block until any pending background save completes.
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> &aSaver)
Register a saver callback invoked during autosave history commits.
bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
void ClearAllSavers()
Clear all registered savers.
bool CommitSnapshot(const std::vector< wxString > &aFiles, const wxString &aTitle)
Commit the given files to the local history repository.
std::atomic< bool > m_saveInProgress
void NoteFileChange(const wxString &aFile)
Record that a file has been modified and should be included in the next snapshot.
bool CommitPending()
Commit any pending modified files to the history repository.
bool HistoryExists(const wxString &aProjectPath)
Return true if history exists for the project.
bool RunRegisteredSaversAsAutosaveFiles(const wxString &aProjectPath)
Run all registered savers and write their output to autosave files instead of committing to the local...
bool CommitFullProjectSnapshot(const wxString &aProjectPath, const wxString &aTitle)
Commit a snapshot of the entire project directory (excluding the .history directory and ignored trans...
std::future< bool > m_pendingFuture
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
void RemoveAutosaveFiles(const wxString &aProjectPath) const
Remove every autosave file under the project at aProjectPath regardless of which source it shadowed.
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
virtual COMMON_SETTINGS * GetCommonSettings() const
virtual SETTINGS_MANAGER & GetSettingsManager() const
A progress reporter interface for use in multi-threaded environments.
virtual void Report(const wxString &aMessage)=0
Display aMessage in the progress bar dialog.
virtual void AdvancePhase()=0
Use the next available virtual zone of the dialog progress bar.
virtual void SetCurrentProgress(double aProgress)=0
Set the progress value to aProgress (0..1).
COMMON_SETTINGS * GetCommonSettings() const
Retrieve the common settings shared by all applications.
wxString GetAutosaveRootForProject(const PROJECT *aProject=nullptr) const
Resolve the autosave-files root for a project.
PROJECT * GetProjectForPath(const wxString &aProjectPath) const
Return the active project iff its path matches aProjectPath, else nullptr.
wxString GetLocalHistoryDirForPath(const wxString &aProjectPath) const
Resolve the local-history directory for a project given by its on-disk path.
@ INCREMENTAL
Git-based local history (default)
@ PROJECT_DIR
Inside the project directory (default)
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
static const std::string LockFileExtension
static const std::string ProjectFileExtension
static const std::string LockFilePrefix
const wxChar *const traceAutoSave
Flag to enable auto save feature debug tracing.
static wxString historyPath(const wxString &aProjectPath)
static wxString historyPath(const wxString &aProjectPath)
static bool compactRepository(git_repository *aRepo, PROGRESS_REPORTER *aReporter=nullptr)
static bool isRestoreProtectedEntry(const wxString &aName)
static std::vector< std::pair< wxString, wxString > > findAutosaveFilePairs(const wxString &aProjectPath)
static const wxString AUTOSAVE_PREFIX
static bool commitSnapshotForProject(const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
static size_t dirSizeRecursive(const wxString &path)
static bool copyTreeObjects(git_repository *aSrcRepo, git_odb *aSrcOdb, git_odb *aDstOdb, const git_oid *aTreeOid, std::set< git_oid, bool(*)(const git_oid &, const git_oid &)> &aCopied)
static bool isKiCadProjectFile(const wxFileName &aFile)
static wxString sourceForAutosaveFile(const wxString &aAutosavePath, const wxString &aProjectPath, const wxString &aAutosaveRoot, BACKUP_LOCATION aLocation)
static wxString resolveAutosaveDestination(const wxString &aAutosaveRoot, const wxString &aRelativePath, BACKUP_LOCATION aLocation)
static SNAPSHOT_COMMIT_RESULT commitSnapshotWithLock(git_repository *repo, git_index *index, const wxString &aHistoryPath, const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
static bool 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...
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.