33#include <wx/datetime.h>
37#include <wx/filename.h>
38#include <wx/stdpaths.h>
47 LIBGIT2_SCOPE() { git_libgit2_init(); }
48 ~LIBGIT2_SCOPE() { git_libgit2_shutdown(); }
52struct SCOPED_BOOL_OVERRIDE
54 explicit SCOPED_BOOL_OVERRIDE(
bool& aFlag ) : m_flag( aFlag ), m_original( aFlag ) {}
55 ~SCOPED_BOOL_OVERRIDE() { m_flag = m_original; }
64struct SCOPED_BACKUP_LOCATION_OVERRIDE
66 explicit SCOPED_BACKUP_LOCATION_OVERRIDE(
BACKUP_LOCATION& aLocation ) :
67 m_location( aLocation ), m_original( aLocation )
71 ~SCOPED_BACKUP_LOCATION_OVERRIDE() { m_location = m_original; }
80struct SCOPED_PROJECT_LOAD
82 SCOPED_PROJECT_LOAD( SETTINGS_MANAGER& aMgr,
const wxString& aProjectFile ) : m_mgr( aMgr )
84 m_mgr.LoadProject( aProjectFile.ToStdString() );
87 ~SCOPED_PROJECT_LOAD() { m_mgr.UnloadProject( &m_mgr.Prj(),
false ); }
89 SETTINGS_MANAGER& m_mgr;
97 explicit SCOPED_TEMP_DIR(
const wxString& aPrefix )
99 wxString base = wxStandardPaths::Get().GetTempDir();
100 m_path = base + wxFileName::GetPathSeparator() + aPrefix
101 + wxString::Format( wxS(
"_%lu_%ld" ),
102 static_cast<unsigned long>( ::wxGetProcessId() ),
103 static_cast<long>( wxDateTime::UNow().GetTicks() ) );
104 wxFileName::Mkdir( m_path, 0777, wxPATH_MKDIR_FULL );
109 if( !m_path.IsEmpty() && wxDirExists( m_path ) )
110 wxFileName::Rmdir( m_path, wxPATH_RMDIR_RECURSIVE );
113 const wxString& Path()
const {
return m_path; }
119void writeTextFile(
const wxString& aPath,
const wxString& aContents )
121 wxFFile f( aPath, wxT(
"w" ) );
123 f.Write( aContents );
144 std::vector<HISTORY_FILE_DATA> fileData;
148 BOOST_CHECK_NO_THROW( board.
SaveToHistory( wxS(
"/tmp/anywhere" ), fileData ) );
149 BOOST_CHECK( fileData.empty() );
161 wxString tempDir = wxStandardPaths::Get().GetTempDir();
162 wxString projectPath = tempDir + wxFileName::GetPathSeparator() + wxS(
"pcb_autosave.kicad_pro" );
169 std::vector<HISTORY_FILE_DATA> fileData;
173 BOOST_CHECK( fileData.empty() );
190 LIBGIT2_SCOPE libgit;
193 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
194 backupEnabled =
true;
196 SCOPED_TEMP_DIR notAProject( wxS(
"kicad_qa_no_project" ) );
197 const wxString&
path = notAProject.Path();
201 wxString boardPath =
path + wxFileName::GetPathSeparator() + wxS(
"stray.kicad_pcb" );
202 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108))\n" ) );
206 BOOST_CHECK( !history.
Init(
path ) );
208 BOOST_CHECK( !history.
TagSave(
path, wxS(
"pcb" ) ) );
211 wxString historyDir =
path + wxFileName::GetPathSeparator() + wxS(
".history" );
212 BOOST_CHECK( !wxDirExists( historyDir ) );
225 LIBGIT2_SCOPE libgit;
228 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
229 backupEnabled =
true;
231 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_subdirs" ) );
234 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"subdirs.kicad_pro" ), wxS(
"{}\n" ) );
235 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"subdirs.kicad_pcb" ),
236 wxS(
"(kicad_pcb (version 20240108))\n" ) );
240 wxString subDir =
path + wxFileName::GetPathSeparator() + wxS(
"libs" );
241 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
242 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS(
"fp.kicad_mod" ),
243 wxS(
"(footprint test)\n" ) );
249 wxString historyDir =
path + wxFileName::GetPathSeparator() + wxS(
".history" );
250 BOOST_CHECK( wxDirExists( historyDir ) );
251 BOOST_CHECK( !wxDirExists( subDir + wxFileName::GetPathSeparator() + wxS(
".history" ) ) );
257 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS(
"fp.kicad_mod" ),
258 wxS(
"(footprint test (modified))\n" ) );
266 BOOST_CHECK( headBefore != headAfter );
273 LIBGIT2_SCOPE libgit;
277 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
278 backupEnabled =
true;
280 SCOPED_TEMP_DIR tempProject( wxS(
"kicad_qa_issue24016" ) );
281 const wxString& projectPath = tempProject.Path();
284 projectPath + wxFileName::GetPathSeparator() + wxS(
"issue24016.kicad_pcb" );
285 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108))\n" ) );
289 wxString projectFile =
290 projectPath + wxFileName::GetPathSeparator() + wxS(
"issue24016.kicad_pro" );
291 writeTextFile( projectFile, wxS(
"{}\n" ) );
296 wxString headHash = history.
GetHeadHash( projectPath );
301 wxString backupsDir =
302 projectPath + wxFileName::GetPathSeparator() + wxS(
"issue24016-backups" );
303 BOOST_REQUIRE( wxFileName::Mkdir( backupsDir, 0777, wxPATH_MKDIR_FULL ) );
305 wxString zipPath = backupsDir + wxFileName::GetPathSeparator()
306 + wxS(
"issue24016-2026-04-22_120000.zip" );
307 writeTextFile( zipPath, wxS(
"pretend-zip-contents" ) );
309 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108) (dirty yes))\n" ) );
314 "zip backups directory must survive RestoreCommit" );
316 ".zip archive inside the backups directory must survive RestoreCommit" );
335 LIBGIT2_SCOPE libgit;
338 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
339 backupEnabled =
true;
341 SCOPED_TEMP_DIR tempProject( wxS(
"kicad_qa_nested_project" ) );
342 const wxString& projectPath = tempProject.Path();
345 wxString parentPro = projectPath + wxFileName::GetPathSeparator() + wxS(
"projectA.kicad_pro" );
346 wxString parentPcb = projectPath + wxFileName::GetPathSeparator() + wxS(
"projectA.kicad_pcb" );
347 writeTextFile( parentPro, wxS(
"{}\n" ) );
348 writeTextFile( parentPcb, wxS(
"(kicad_pcb (version 20240108))\n" ) );
353 wxString headHash = history.
GetHeadHash( projectPath );
359 wxString nestedDir = projectPath + wxFileName::GetPathSeparator() + wxS(
"projectB" );
360 BOOST_REQUIRE( wxFileName::Mkdir( nestedDir, 0777, wxPATH_MKDIR_FULL ) );
362 wxString nestedPro = nestedDir + wxFileName::GetPathSeparator() + wxS(
"projectB.kicad_pro" );
363 wxString nestedPcb = nestedDir + wxFileName::GetPathSeparator() + wxS(
"projectB.kicad_pcb" );
364 wxString nestedSch = nestedDir + wxFileName::GetPathSeparator() + wxS(
"projectB.kicad_sch" );
365 writeTextFile( nestedPro, wxS(
"{ \"nested\": true }\n" ) );
366 writeTextFile( nestedPcb, wxS(
"(kicad_pcb (version 20240108) (nested yes))\n" ) );
367 writeTextFile( nestedSch, wxS(
"(kicad_sch (version 20240108))\n" ) );
370 writeTextFile( parentPcb, wxS(
"(kicad_pcb (version 20240108) (dirty yes))\n" ) );
376 "nested project directory must survive RestoreCommit" );
378 "nested .kicad_pro must survive RestoreCommit" );
380 "nested .kicad_pcb must survive RestoreCommit" );
382 "nested .kicad_sch must survive RestoreCommit" );
385 BOOST_CHECK( wxFileExists( parentPro ) );
386 BOOST_CHECK( wxFileExists( parentPcb ) );
397 LIBGIT2_SCOPE libgit;
400 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
401 backupEnabled =
true;
403 SCOPED_TEMP_DIR tempProject( wxS(
"kicad_qa_retained_backup" ) );
404 const wxString& projectPath = tempProject.Path();
406 wxString boardPath = projectPath + wxFileName::GetPathSeparator() + wxS(
"rb.kicad_pcb" );
407 wxString projectFile = projectPath + wxFileName::GetPathSeparator() + wxS(
"rb.kicad_pro" );
408 writeTextFile( projectFile, wxS(
"{}\n" ) );
409 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108))\n" ) );
414 wxString headHash = history.
GetHeadHash( projectPath );
418 writeTextFile( boardPath, wxS(
"(kicad_pcb (version 20240108) (dirty yes))\n" ) );
424 wxString parentDir = wxFileName( projectPath ).GetPath();
425 wxString leafPrefix = wxFileName( projectPath ).GetFullName() + wxS(
"_restore_backup_" );
427 wxDir dir( parentDir );
430 wxString retainedBackup;
431 bool foundLegacyBackup =
false;
434 for(
bool cont = dir.GetFirst( &
name, wxEmptyString, wxDIR_DIRS ); cont;
435 cont = dir.GetNext( &
name ) )
437 if(
name.StartsWith( leafPrefix ) )
439 retainedBackup = parentDir + wxFileName::GetPathSeparator() +
name;
443 "retained backup directory name must not contain ':' "
444 "(Windows-illegal in path components)" );
446 else if(
name == wxFileName( projectPath ).GetFullName() + wxS(
"_restore_backup" ) )
448 foundLegacyBackup =
true;
453 !retainedBackup.IsEmpty(),
454 "RestoreCommit must retain a timestamped _restore_backup_<ts>/ sibling directory" );
457 "RestoreCommit must not leave the legacy non-timestamped _restore_backup/ behind" );
460 if( !retainedBackup.IsEmpty() && wxDirExists( retainedBackup ) )
461 wxFileName::Rmdir( retainedBackup, wxPATH_RMDIR_RECURSIVE );
470 LIBGIT2_SCOPE libgit;
473 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
474 backupEnabled =
true;
476 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_privacy" ) );
479 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
480 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pcb" ),
481 wxS(
"(kicad_pcb (version 20240108))\n" ) );
482 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_sch" ),
483 wxS(
"(kicad_sch (version 20240108))\n" ) );
485 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"passwords.txt" ), wxS(
"secret\n" ) );
486 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"datasheet.pdf" ), wxS(
"fake pdf bytes\n" ) );
487 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"notes.md" ), wxS(
"personal notes\n" ) );
489 wxString subDir =
path + wxFileName::GetPathSeparator() + wxS(
"docs" );
490 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
491 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS(
"manual.txt" ), wxS(
"irrelevant\n" ) );
496 wxString hist =
path + wxFileName::GetPathSeparator() + wxS(
".history" );
497 git_repository* repo =
nullptr;
498 BOOST_REQUIRE_EQUAL( git_repository_open( &repo, hist.mb_str().data() ), 0 );
501 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &head_oid, repo,
"HEAD" ), 0 );
503 git_commit* head =
nullptr;
504 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &head_oid ), 0 );
506 git_tree* tree =
nullptr;
507 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), 0 );
509 std::vector<std::string> committedPaths;
511 tree, GIT_TREEWALK_PRE,
512 [](
const char* root,
const git_tree_entry* entry,
void* payload ) ->
int
514 auto* paths =
static_cast<std::vector<std::string>*
>( payload );
516 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
517 paths->push_back( std::string( root ) + git_tree_entry_name( entry ) );
523 git_tree_free( tree );
524 git_commit_free( head );
525 git_repository_free( repo );
527 auto contains = [&](
const std::string& s )
529 return std::find( committedPaths.begin(), committedPaths.end(), s ) != committedPaths.end();
536 BOOST_CHECK_MESSAGE( !contains(
"passwords.txt" ),
"passwords.txt must NOT appear in history" );
537 BOOST_CHECK_MESSAGE( !contains(
"datasheet.pdf" ),
"datasheet.pdf must NOT appear in history" );
539 BOOST_CHECK_MESSAGE( !contains(
"docs/manual.txt" ),
"subdirectory user content must NOT appear in history" );
549 LIBGIT2_SCOPE libgit;
552 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
553 backupEnabled =
true;
555 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_first_idle_autosave" ) );
558 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
559 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pcb" ),
560 wxS(
"(kicad_pcb (version 20240108))\n" ) );
565 std::string inMemoryContent =
"(kicad_pcb (version 20240108))\n";
567 auto saver = [&inMemoryContent](
const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
571 entry.
content = inMemoryContent;
572 aFileData.push_back( std::move( entry ) );
581 wxString histDir =
path + wxFileName::GetPathSeparator() + wxS(
".history" );
582 BOOST_CHECK( wxDirExists( histDir ) );
585 BOOST_CHECK_MESSAGE( head.IsEmpty(),
"no untagged HEAD should exist after an idle first save" );
588 inMemoryContent =
"(kicad_pcb (version 20240108) (edited yes))\n";
594 BOOST_CHECK_MESSAGE( !head.IsEmpty(),
"in-memory edits diverging from disk must produce a commit" );
607 LIBGIT2_SCOPE libgit;
610 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
611 backupEnabled =
true;
613 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_first_manual_save" ) );
616 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
617 writeTextFile(
path + wxFileName::GetPathSeparator() + wxS(
"p.kicad_pcb" ),
618 wxS(
"(kicad_pcb (version 20240108))\n" ) );
622 std::string inMemoryContent =
"(kicad_pcb (version 20240108))\n";
624 auto saver = [&inMemoryContent](
const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
628 entry.
content = inMemoryContent;
629 aFileData.push_back( std::move( entry ) );
638 BOOST_CHECK_MESSAGE( !head.IsEmpty(),
"manual save on a fresh project must commit even when staged matches disk" );
649 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
650 backupEnabled =
true;
653 SCOPED_BACKUP_LOCATION_OVERRIDE restoreLocation(
location );
656 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_cloudsync_autosave" ) );
658 const wxString sep = wxFileName::GetPathSeparator();
662 writeTextFile(
path + sep + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
665 SCOPED_PROJECT_LOAD loadedProject( mgr,
path + sep + wxS(
"p.kicad_pro" ) );
667 const wxString sourcePath =
path + sep + wxS(
"p.kicad_pcb" );
668 const wxString autosavePath =
path + sep + wxS(
"_autosave-p.kicad_pcb" );
669 const wxString boardContent = wxS(
"(kicad_pcb (version 20240108))\n" );
672 writeTextFile( sourcePath, boardContent );
673 writeTextFile( autosavePath, boardContent );
678 wxDateTime srcMtime = wxFileName( sourcePath ).GetModificationTime();
679 wxDateTime newerMtime = srcMtime + wxTimeSpan::Seconds( 60 );
680 BOOST_REQUIRE( wxFileName( autosavePath ).SetTimes( &newerMtime, &newerMtime,
nullptr ) );
682 std::vector<wxString> exts{ wxS(
"kicad_pcb" ) };
686 "Content-identical autosave with newer mtime was flagged stale, "
687 "triggering a spurious recovery prompt (issue 24126)" );
696 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
697 backupEnabled =
true;
700 SCOPED_BACKUP_LOCATION_OVERRIDE restoreLocation(
location );
703 SCOPED_TEMP_DIR
project( wxS(
"kicad_qa_divergent_autosave" ) );
705 const wxString sep = wxFileName::GetPathSeparator();
707 writeTextFile(
path + sep + wxS(
"p.kicad_pro" ), wxS(
"{}\n" ) );
710 SCOPED_PROJECT_LOAD loadedProject( mgr,
path + sep + wxS(
"p.kicad_pro" ) );
712 const wxString sourcePath =
path + sep + wxS(
"p.kicad_pcb" );
713 const wxString autosavePath =
path + sep + wxS(
"_autosave-p.kicad_pcb" );
716 writeTextFile( sourcePath, wxS(
"(kicad_pcb (version 20240108))\n" ) );
717 writeTextFile( autosavePath, wxS(
"(kicad_pcb (version 20240108) (edited yes))\n" ) );
721 wxDateTime srcMtime = wxFileName( sourcePath ).GetModificationTime();
722 wxDateTime newerMtime = srcMtime + wxTimeSpan::Seconds( 60 );
723 BOOST_REQUIRE( wxFileName( autosavePath ).SetTimes( &newerMtime, &newerMtime,
nullptr ) );
725 std::vector<wxString> exts{ wxS(
"kicad_pcb" ) };
729 "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")