34#include <wx/datetime.h>
38#include <wx/filename.h>
39#include <wx/stdpaths.h>
48 LIBGIT2_SCOPE() { git_libgit2_init(); }
49 ~LIBGIT2_SCOPE() { git_libgit2_shutdown(); }
53struct SCOPED_BOOL_OVERRIDE
55 explicit SCOPED_BOOL_OVERRIDE(
bool& aFlag ) : m_flag( aFlag ), m_original( aFlag ) {}
56 ~SCOPED_BOOL_OVERRIDE() { m_flag = m_original; }
65struct SCOPED_BACKUP_LOCATION_OVERRIDE
67 explicit SCOPED_BACKUP_LOCATION_OVERRIDE(
BACKUP_LOCATION& aLocation ) :
68 m_location( aLocation ), m_original( aLocation )
72 ~SCOPED_BACKUP_LOCATION_OVERRIDE() { m_location = m_original; }
81struct SCOPED_PROJECT_LOAD
83 SCOPED_PROJECT_LOAD( SETTINGS_MANAGER& aMgr,
const wxString& aProjectFile ) : m_mgr( aMgr )
85 m_mgr.LoadProject( aProjectFile.ToStdString() );
88 ~SCOPED_PROJECT_LOAD() { m_mgr.UnloadProject( &m_mgr.Prj(),
false ); }
90 SETTINGS_MANAGER& m_mgr;
98 explicit SCOPED_TEMP_DIR(
const wxString& aPrefix )
100 wxString base = wxStandardPaths::Get().GetTempDir();
101 m_path = base + wxFileName::GetPathSeparator() + aPrefix
102 + wxString::Format( wxS(
"_%lu_%ld" ),
103 static_cast<unsigned long>( ::wxGetProcessId() ),
104 static_cast<long>( wxDateTime::UNow().GetTicks() ) );
105 wxFileName::Mkdir( m_path, 0777, wxPATH_MKDIR_FULL );
110 if( !m_path.IsEmpty() && wxDirExists( m_path ) )
111 wxFileName::Rmdir( m_path, wxPATH_RMDIR_RECURSIVE );
114 const wxString& Path()
const {
return m_path; }
120void writeTextFile(
const wxString& aPath,
const wxString& aContents )
122 wxFFile f( aPath, wxT(
"w" ) );
124 f.Write( aContents );
145 std::vector<HISTORY_FILE_DATA> fileData;
149 BOOST_CHECK_NO_THROW( board.
SaveToHistory( wxS(
"/tmp/anywhere" ), fileData ) );
150 BOOST_CHECK( fileData.empty() );
162 wxString tempDir = wxStandardPaths::Get().GetTempDir();
163 wxString projectPath = tempDir + wxFileName::GetPathSeparator() + wxS(
"pcb_autosave.kicad_pro" );
170 std::vector<HISTORY_FILE_DATA> fileData;
174 BOOST_CHECK( fileData.empty() );
191 LIBGIT2_SCOPE libgit;
194 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
195 backupEnabled =
true;
197 SCOPED_TEMP_DIR notAProject( wxS(
"kicad_qa_no_project" ) );
198 const wxString&
path = notAProject.Path();
202 wxString boardPath =
path + wxFileName::GetPathSeparator() + wxS(
"stray.kicad_pcb" );
203 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108))\n" ) );
207 BOOST_CHECK( !history.
Init(
path ) );
209 BOOST_CHECK( !history.
TagSave(
path, wxS(
"pcb" ) ) );
212 wxString historyDir =
path + wxFileName::GetPathSeparator() + wxS(
".history" );
213 BOOST_CHECK( !wxDirExists( historyDir ) );
226 LIBGIT2_SCOPE libgit;
229 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
230 backupEnabled =
true;
232 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_subdirs" ) );
235 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"subdirs.kicad_pro" ), wxS(
"{}\n" ) );
236 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"subdirs.kicad_pcb" ),
237 wxS(
"(kicad_pcb (version 20240108))\n" ) );
241 wxString subDir =
path + wxFileName::GetPathSeparator() + wxS(
"libs" );
242 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
243 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS(
"fp.kicad_mod" ),
244 wxS(
"(footprint test)\n" ) );
250 wxString historyDir =
path + wxFileName::GetPathSeparator() + wxS(
".history" );
251 BOOST_CHECK( wxDirExists( historyDir ) );
252 BOOST_CHECK( !wxDirExists( subDir + wxFileName::GetPathSeparator() + wxS(
".history" ) ) );
258 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS(
"fp.kicad_mod" ),
259 wxS(
"(footprint test (modified))\n" ) );
267 BOOST_CHECK( headBefore != headAfter );
274 LIBGIT2_SCOPE libgit;
278 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
279 backupEnabled =
true;
281 SCOPED_TEMP_DIR tempProject( wxS(
"kicad_qa_issue24016" ) );
282 const wxString& projectPath = tempProject.Path();
285 projectPath + wxFileName::GetPathSeparator() + wxS(
"issue24016.kicad_pcb" );
286 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108))\n" ) );
290 wxString projectFile =
291 projectPath + wxFileName::GetPathSeparator() + wxS(
"issue24016.kicad_pro" );
292 writeTextFile( projectFile, wxS(
"{}\n" ) );
297 wxString headHash = history.
GetHeadHash( projectPath );
302 wxString backupsDir =
303 projectPath + wxFileName::GetPathSeparator() + wxS(
"issue24016-backups" );
304 BOOST_REQUIRE( wxFileName::Mkdir( backupsDir, 0777, wxPATH_MKDIR_FULL ) );
306 wxString zipPath = backupsDir + wxFileName::GetPathSeparator()
307 + wxS(
"issue24016-2026-04-22_120000.zip" );
308 writeTextFile( zipPath, wxS(
"pretend-zip-contents" ) );
310 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108) (dirty yes))\n" ) );
315 "zip backups directory must survive RestoreCommit" );
317 ".zip archive inside the backups directory must survive RestoreCommit" );
336 LIBGIT2_SCOPE libgit;
339 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
340 backupEnabled =
true;
342 SCOPED_TEMP_DIR tempProject( wxS(
"kicad_qa_nested_project" ) );
343 const wxString& projectPath = tempProject.Path();
346 wxString parentPro = projectPath + wxFileName::GetPathSeparator() + wxS(
"projectA.kicad_pro" );
347 wxString parentPcb = projectPath + wxFileName::GetPathSeparator() + wxS(
"projectA.kicad_pcb" );
348 writeTextFile( parentPro, wxS(
"{}\n" ) );
349 writeTextFile( parentPcb, wxS(
"(kicad_pcb (version 20240108))\n" ) );
354 wxString headHash = history.
GetHeadHash( projectPath );
360 wxString nestedDir = projectPath + wxFileName::GetPathSeparator() + wxS(
"projectB" );
361 BOOST_REQUIRE( wxFileName::Mkdir( nestedDir, 0777, wxPATH_MKDIR_FULL ) );
363 wxString nestedPro = nestedDir + wxFileName::GetPathSeparator() + wxS(
"projectB.kicad_pro" );
364 wxString nestedPcb = nestedDir + wxFileName::GetPathSeparator() + wxS(
"projectB.kicad_pcb" );
365 wxString nestedSch = nestedDir + wxFileName::GetPathSeparator() + wxS(
"projectB.kicad_sch" );
366 writeTextFile( nestedPro, wxS(
"{ \"nested\": true }\n" ) );
367 writeTextFile( nestedPcb, wxS(
"(kicad_pcb (version 20240108) (nested yes))\n" ) );
368 writeTextFile( nestedSch, wxS(
"(kicad_sch (version 20240108))\n" ) );
371 writeTextFile( parentPcb, wxS(
"(kicad_pcb (version 20240108) (dirty yes))\n" ) );
377 "nested project directory must survive RestoreCommit" );
379 "nested .kicad_pro must survive RestoreCommit" );
381 "nested .kicad_pcb must survive RestoreCommit" );
383 "nested .kicad_sch must survive RestoreCommit" );
386 BOOST_CHECK( wxFileExists( parentPro ) );
387 BOOST_CHECK( wxFileExists( parentPcb ) );
398 LIBGIT2_SCOPE libgit;
401 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
402 backupEnabled =
true;
404 SCOPED_TEMP_DIR tempProject( wxS(
"kicad_qa_retained_backup" ) );
405 const wxString& projectPath = tempProject.Path();
407 wxString boardPath = projectPath + wxFileName::GetPathSeparator() + wxS(
"rb.kicad_pcb" );
408 wxString projectFile = projectPath + wxFileName::GetPathSeparator() + wxS(
"rb.kicad_pro" );
409 writeTextFile( projectFile, wxS(
"{}\n" ) );
410 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108))\n" ) );
415 wxString headHash = history.
GetHeadHash( projectPath );
419 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108) (dirty yes))\n" ) );
425 wxString parentDir = wxFileName( projectPath ).GetPath();
426 wxString leafPrefix = wxFileName( projectPath ).GetFullName() + wxS(
"_restore_backup_" );
428 wxDir dir( parentDir );
431 wxString retainedBackup;
432 bool foundLegacyBackup =
false;
435 for(
bool cont = dir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS ); cont;
436 cont = dir.GetNext( &
name ) )
438 if(
name.StartsWith( leafPrefix ) )
440 retainedBackup = parentDir + wxFileName::GetPathSeparator() +
name;
444 "retained backup directory name must not contain ':' "
445 "(Windows-illegal in path components)" );
447 else if(
name == wxFileName( projectPath ).GetFullName() + wxS(
"_restore_backup" ) )
449 foundLegacyBackup =
true;
454 !retainedBackup.IsEmpty(),
455 "RestoreCommit must retain a timestamped _restore_backup_<ts>/ sibling directory" );
458 "RestoreCommit must not leave the legacy non-timestamped _restore_backup/ behind" );
461 if( !retainedBackup.IsEmpty() && wxDirExists( retainedBackup ) )
462 wxFileName::Rmdir( retainedBackup, wxPATH_RMDIR_RECURSIVE );
471 LIBGIT2_SCOPE libgit;
474 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
475 backupEnabled =
true;
477 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_privacy" ) );
480 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
481 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pcb" ),
482 wxS(
"(kicad_pcb (version 20240108))\n" ) );
483 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_sch" ),
484 wxS(
"(kicad_sch (version 20240108))\n" ) );
486 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"passwords.txt" ), wxS(
"secret\n" ) );
487 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"datasheet.pdf" ), wxS(
"fake pdf bytes\n" ) );
488 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"notes.md" ), wxS(
"personal notes\n" ) );
490 wxString subDir =
path + wxFileName::GetPathSeparator() + wxS(
"docs" );
491 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
492 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS(
"manual.txt" ), wxS(
"irrelevant\n" ) );
497 wxString hist =
path + wxFileName::GetPathSeparator() + wxS(
".history" );
498 git_repository* repo =
nullptr;
499 BOOST_REQUIRE_EQUAL( git_repository_open( &repo, hist.mb_str().data() ), 0 );
502 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &head_oid, repo,
"HEAD" ), 0 );
504 git_commit* head =
nullptr;
505 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &head_oid ), 0 );
507 git_tree* tree =
nullptr;
508 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), 0 );
510 std::vector<std::string> committedPaths;
512 tree, GIT_TREEWALK_PRE,
513 [](
const char* root,
const git_tree_entry* entry,
void* payload ) ->
int
515 auto* paths =
static_cast<std::vector<std::string>*
>( payload );
517 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
518 paths->push_back( std::string( root ) + git_tree_entry_name( entry ) );
524 git_tree_free( tree );
525 git_commit_free( head );
526 git_repository_free( repo );
528 auto contains = [&](
const std::string& s )
530 return std::find( committedPaths.begin(), committedPaths.end(), s ) != committedPaths.end();
537 BOOST_CHECK_MESSAGE( !contains(
"passwords.txt" ),
"passwords.txt must NOT appear in history" );
538 BOOST_CHECK_MESSAGE( !contains(
"datasheet.pdf" ),
"datasheet.pdf must NOT appear in history" );
540 BOOST_CHECK_MESSAGE( !contains(
"docs/manual.txt" ),
"subdirectory user content must NOT appear in history" );
550 LIBGIT2_SCOPE libgit;
553 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
554 backupEnabled =
true;
556 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_first_idle_autosave" ) );
559 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
560 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pcb" ),
561 wxS(
"(kicad_pcb (version 20240108))\n" ) );
566 std::string inMemoryContent =
"(kicad_pcb (version 20240108))\n";
568 auto saver = [&inMemoryContent](
const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
572 entry.
content = inMemoryContent;
573 aFileData.push_back( std::move( entry ) );
582 wxString histDir =
path + wxFileName::GetPathSeparator() + wxS(
".history" );
583 BOOST_CHECK( wxDirExists( histDir ) );
586 BOOST_CHECK_MESSAGE( head.IsEmpty(),
"no untagged HEAD should exist after an idle first save" );
589 inMemoryContent =
"(kicad_pcb (version 20240108) (edited yes))\n";
595 BOOST_CHECK_MESSAGE( !head.IsEmpty(),
"in-memory edits diverging from disk must produce a commit" );
608 LIBGIT2_SCOPE libgit;
611 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
612 backupEnabled =
true;
614 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_first_manual_save" ) );
617 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
618 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pcb" ),
619 wxS(
"(kicad_pcb (version 20240108))\n" ) );
623 std::string inMemoryContent =
"(kicad_pcb (version 20240108))\n";
625 auto saver = [&inMemoryContent](
const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
629 entry.
content = inMemoryContent;
630 aFileData.push_back( std::move( entry ) );
639 BOOST_CHECK_MESSAGE( !head.IsEmpty(),
"manual save on a fresh project must commit even when staged matches disk" );
650 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
651 backupEnabled =
true;
654 SCOPED_BACKUP_LOCATION_OVERRIDE restoreLocation(
location );
657 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_cloudsync_autosave" ) );
659 const wxString sep = wxFileName::GetPathSeparator();
663 writeTextFile(
path + sep + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
666 SCOPED_PROJECT_LOAD loadedProject( mgr,
path + sep + wxS(
"p.kicad_pro" ) );
668 const wxString sourcePath =
path + sep + wxS(
"p.kicad_pcb" );
669 const wxString autosavePath =
path + sep + wxS(
"_autosave-p.kicad_pcb" );
670 const wxString boardContent = wxS(
"(kicad_pcb (version 20240108))\n" );
673 writeTextFile( sourcePath, boardContent );
674 writeTextFile( autosavePath, boardContent );
679 wxDateTime srcMtime = wxFileName( sourcePath ).GetModificationTime();
680 wxDateTime newerMtime = srcMtime + wxTimeSpan::Seconds( 60 );
681 BOOST_REQUIRE( wxFileName( autosavePath ).SetTimes( &newerMtime, &newerMtime,
nullptr ) );
683 std::vector<wxString> exts{ wxS(
"kicad_pcb" ) };
687 "Content-identical autosave with newer mtime was flagged stale, "
688 "triggering a spurious recovery prompt (issue 24126)" );
697 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
698 backupEnabled =
true;
701 SCOPED_BACKUP_LOCATION_OVERRIDE restoreLocation(
location );
704 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_divergent_autosave" ) );
706 const wxString sep = wxFileName::GetPathSeparator();
708 writeTextFile(
path + sep + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
711 SCOPED_PROJECT_LOAD loadedProject( mgr,
path + sep + wxS(
"p.kicad_pro" ) );
713 const wxString sourcePath =
path + sep + wxS(
"p.kicad_pcb" );
714 const wxString autosavePath =
path + sep + wxS(
"_autosave-p.kicad_pcb" );
717 writeTextFile( sourcePath, wxS(
"(kicad_pcb (version 20240108))\n" ) );
718 writeTextFile( autosavePath, wxS(
"(kicad_pcb (version 20240108) (edited yes))\n" ) );
722 wxDateTime srcMtime = wxFileName( sourcePath ).GetModificationTime();
723 wxDateTime newerMtime = srcMtime + wxTimeSpan::Seconds( 60 );
724 BOOST_REQUIRE( wxFileName( autosavePath ).SetTimes( &newerMtime, &newerMtime,
nullptr ) );
726 std::vector<wxString> exts{ wxS(
"kicad_pcb" ) };
730 "Genuinely divergent autosave must still be flagged stale for recovery" );
Information pertinent to a Pcbnew printed circuit board.
void SaveToHistory(const wxString &aProjectPath, std::vector< HISTORY_FILE_DATA > &aFileData)
Serialize board into HISTORY_FILE_DATA for non-blocking history commit.
void SetProject(PROJECT *aProject, bool aReferenceOnly=false)
Link a board to a given project.
const wxString & GetFileName() const
PROJECT * GetProject() const
Simple local history manager built on libgit2.
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.
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.
bool CommitFullProjectSnapshot(const wxString &aProjectPath, const wxString &aTitle)
Commit a snapshot of the entire project directory (excluding the .history directory and ignored trans...
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
virtual COMMON_SETTINGS * GetCommonSettings() const
virtual SETTINGS_MANAGER & GetSettingsManager() const
virtual const wxString GetProjectPath() const
Return the full path of the project.
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
bool UnloadProject(PROJECT *aProject, bool aSave=true)
Save, unload and unregister the given PROJECT.
PROJECT & Prj() const
A helper while we are not MDI-capable – return the one and only project.
@ PROJECT_DIR
Inside the project directory (default)
PGM_BASE & Pgm()
The global program "get" accessor.
BACKUP_LOCATION location
Where backups, history, and autosave files live.
bool enabled
Automatically back up the project when files are saved.
Data produced by a registered saver on the UI thread, consumed by either the background local-history...
std::string content
Serialized content (mutually exclusive with sourcePath)
wxString relativePath
Destination path relative to the project root.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(SaveToHistoryWithNullProjectDoesNotCrash)
Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23737.
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")