KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_history_autosave.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21#include <qa_utils/file_utils.h>
22
23#include <board.h>
25#include <local_history.h>
26#include <pgm_base.h>
27#include <project.h>
31
32#include <git2.h>
33
34#include <algorithm>
35#include <memory>
36#include <vector>
37
38#include <wx/datetime.h>
39#include <wx/dir.h>
40#include <wx/ffile.h>
41#include <wx/filefn.h>
42#include <wx/filename.h>
43#include <wx/stdpaths.h>
44
45
46namespace
47{
48// single_top handles libgit2 init in production; the QA harness does not, so tests driving
49// LOCAL_HISTORY must manage it themselves.
50struct LIBGIT2_SCOPE
51{
52 LIBGIT2_SCOPE() { git_libgit2_init(); }
53 ~LIBGIT2_SCOPE() { git_libgit2_shutdown(); }
54};
55
56
57struct SCOPED_BOOL_OVERRIDE
58{
59 explicit SCOPED_BOOL_OVERRIDE( bool& aFlag ) : m_flag( aFlag ), m_original( aFlag ) {}
60 ~SCOPED_BOOL_OVERRIDE() { m_flag = m_original; }
61
62 bool& m_flag;
63 bool m_original;
64};
65
66
67// Restore the backup location on destruction so a thrown BOOST_REQUIRE cannot leak the
68// override into later tests.
69struct SCOPED_BACKUP_LOCATION_OVERRIDE
70{
71 explicit SCOPED_BACKUP_LOCATION_OVERRIDE( BACKUP_LOCATION& aLocation ) :
72 m_location( aLocation ), m_original( aLocation )
73 {
74 }
75
76 ~SCOPED_BACKUP_LOCATION_OVERRIDE() { m_location = m_original; }
77
78 BACKUP_LOCATION& m_location;
79 BACKUP_LOCATION m_original;
80};
81
82
83struct SCOPED_BACKUP_FORMAT_OVERRIDE
84{
85 explicit SCOPED_BACKUP_FORMAT_OVERRIDE( BACKUP_FORMAT& aFormat ) :
86 m_format( aFormat ), m_original( aFormat )
87 {
88 }
89
90 ~SCOPED_BACKUP_FORMAT_OVERRIDE() { m_format = m_original; }
91
92 BACKUP_FORMAT& m_format;
93 BACKUP_FORMAT m_original;
94};
95
96
97// Load a project into the settings manager and unload it on destruction, keeping the global
98// active-project state isolated even when a test aborts partway through.
99struct SCOPED_PROJECT_LOAD
100{
101 SCOPED_PROJECT_LOAD( SETTINGS_MANAGER& aMgr, const wxString& aProjectFile ) : m_mgr( aMgr )
102 {
103 m_mgr.LoadProject( aProjectFile.ToStdString() );
104 }
105
106 ~SCOPED_PROJECT_LOAD() { m_mgr.UnloadProject( &m_mgr.Prj(), false ); }
107
108 SETTINGS_MANAGER& m_mgr;
109};
110
111
112void writeTextFile( const wxString& aPath, const wxString& aContents )
113{
114 wxFFile f( aPath, wxT( "w" ) );
115 BOOST_REQUIRE( f.IsOpened() );
116 f.Write( aContents );
117 f.Close();
118}
119
120
121std::string readTextFile( const wxString& aPath )
122{
123 wxFFile f( aPath, wxT( "rb" ) );
124 BOOST_REQUIRE( f.IsOpened() );
125
126 wxString text;
127 BOOST_REQUIRE( f.ReadAll( &text ) );
128
129 return text.ToStdString();
130}
131
132
133std::string historyEntryBytes( const HISTORY_FILE_DATA& aEntry )
134{
135 if( !aEntry.content.empty() )
136 return aEntry.content;
137
138 if( !aEntry.sourcePath.IsEmpty() )
139 return readTextFile( aEntry.sourcePath );
140
141 return std::string();
142}
143
144
145// One project seeded on disk and loaded, shared by the snapshot tests.
146struct SNAPSHOT_PROJECT
147{
148 SNAPSHOT_PROJECT( const wxString& aDirPrefix, const wxString& aFileName ) :
149 m_tempDir( aDirPrefix ),
150 m_proPath( m_tempDir.ChildPathStr( aFileName ) )
151 {
152 writeTextFile( m_proPath, wxS( "{ \"meta\": { \"version\": 3 } }\n" ) );
153 m_project = std::make_unique<SCOPED_PROJECT_LOAD>( m_mgr, m_proPath );
154 }
155
156 PROJECT& Project() { return m_mgr.Prj(); }
157 PROJECT_FILE& ProjectFile() { return m_mgr.Prj().GetProjectFile(); }
158
159 SETTINGS_MANAGER m_mgr;
160 KI_TEST::SCOPED_TEMP_DIR m_tempDir;
161 wxString m_proPath;
162 std::unique_ptr<SCOPED_PROJECT_LOAD> m_project;
163};
164
165
166void addViaStackPreset( BOARD_DESIGN_SETTINGS& aSettings, const wxString& aName )
167{
168 VIA_STACK_PRESET preset;
169 preset.m_Name = aName;
170 aSettings.m_ViaStackPresets.push_back( preset );
171}
172
173
174// Snapshot the project and return the bytes recorded for the .kicad_pro.
175std::string snapshotProjectBytes( PROJECT& aProject )
176{
177 std::vector<HISTORY_FILE_DATA> fileData;
178 aProject.SaveToHistory( aProject.GetProjectFullName(), fileData );
179
180 auto it = std::find_if( fileData.begin(), fileData.end(),
181 []( const HISTORY_FILE_DATA& aEntry )
182 {
183 return aEntry.relativePath.EndsWith( wxS( ".kicad_pro" ) );
184 } );
185
186 BOOST_REQUIRE( it != fileData.end() );
187
188 return historyEntryBytes( *it );
189}
190} // namespace
191
192
193BOOST_AUTO_TEST_SUITE( PcbHistoryAutosave )
194
195
196
205BOOST_AUTO_TEST_CASE( SaveToHistoryWithNullProjectDoesNotCrash )
206{
207 BOARD board;
208 std::vector<HISTORY_FILE_DATA> fileData;
209
210 BOOST_REQUIRE( board.GetProject() == nullptr );
211
212 BOOST_CHECK_NO_THROW( board.SaveToHistory( wxS( "/tmp/anywhere" ), fileData ) );
213 BOOST_CHECK( fileData.empty() );
214}
215
216
221BOOST_AUTO_TEST_CASE( SaveToHistoryUnsavedBoardProducesNothing )
222{
224
225 wxString tempDir = wxStandardPaths::Get().GetTempDir();
226 wxString projectPath = tempDir + wxFileName::GetPathSeparator() + wxS( "pcb_autosave.kicad_pro" );
227
228 mgr.LoadProject( projectPath.ToStdString() );
229
230 BOARD board;
231 board.SetProject( &mgr.Prj() );
232
233 std::vector<HISTORY_FILE_DATA> fileData;
234
235 BOOST_REQUIRE( board.GetFileName().IsEmpty() );
236 BOOST_CHECK_NO_THROW( board.SaveToHistory( mgr.Prj().GetProjectPath(), fileData ) );
237 BOOST_CHECK( fileData.empty() );
238
239 // Detach project before BOARD destruction so design settings ownership unwinds cleanly
240 board.ClearProject();
241 mgr.UnloadProject( &mgr.Prj(), false );
242}
243
244
252BOOST_AUTO_TEST_CASE( NoSnapshotWithoutProjectFile )
253{
254 LIBGIT2_SCOPE libgit;
255
256 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
257 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
258 backupEnabled = true;
259
260 KI_TEST::SCOPED_TEMP_DIR notAProject( wxS( "kicad_qa_no_project" ) );
261 const wxString& path = notAProject.PathStr();
262
263 // Drop a board file in but no .kicad_pro - this mirrors saving a board to /tmp
264 // from standalone pcbnew.
265 wxString boardPath = path + wxFileName::GetPathSeparator() + wxS( "stray.kicad_pcb" );
266 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108))\n" ) );
267
268 LOCAL_HISTORY history;
269
270 BOOST_CHECK( !history.Init( path ) );
271 BOOST_CHECK( !history.CommitFullProjectSnapshot( path, wxS( "PCB Save" ) ) );
272 BOOST_CHECK( !history.TagSave( path, wxS( "pcb" ) ) );
273
274 // No .history directory should have been created.
275 wxString historyDir = path + wxFileName::GetPathSeparator() + wxS( ".history" );
276 BOOST_CHECK( !wxDirExists( historyDir ) );
277}
278
279
287BOOST_AUTO_TEST_CASE( CommitFullProjectSnapshotHandlesSubdirectories )
288{
289 LIBGIT2_SCOPE libgit;
290
291 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
292 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
293 backupEnabled = true;
294
295 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_subdirs" ) );
296 const wxString& path = project.PathStr();
297
298 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "subdirs.kicad_pro" ), wxS( "{}\n" ) );
299 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "subdirs.kicad_pcb" ),
300 wxS( "(kicad_pcb (version 20240108))\n" ) );
301
302 // Create a subdirectory whose contents will likely be collected before files at the
303 // project root, exercising the subdirectory-first iteration order.
304 wxString subDir = path + wxFileName::GetPathSeparator() + wxS( "libs" );
305 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
306 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS( "fp.kicad_mod" ),
307 wxS( "(footprint test)\n" ) );
308
309 LOCAL_HISTORY history;
310 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Initial" ) ) );
311
312 // History must have been created at the project root, not at the subdirectory.
313 wxString historyDir = path + wxFileName::GetPathSeparator() + wxS( ".history" );
314 BOOST_CHECK( wxDirExists( historyDir ) );
315 BOOST_CHECK( !wxDirExists( subDir + wxFileName::GetPathSeparator() + wxS( ".history" ) ) );
316
317 wxString headBefore = history.GetHeadHash( path );
318 BOOST_REQUIRE( !headBefore.IsEmpty() );
319
320 // Mutate a file in the subdirectory to ensure the next snapshot has work to do.
321 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS( "fp.kicad_mod" ),
322 wxS( "(footprint test (modified))\n" ) );
323
324 // CommitFullProjectSnapshot must commit using the project root, not derive a wrong
325 // root from the first file in the recursive collection.
326 BOOST_CHECK( history.CommitFullProjectSnapshot( path, wxS( "PCB Save" ) ) );
327
328 wxString headAfter = history.GetHeadHash( path );
329 BOOST_REQUIRE( !headAfter.IsEmpty() );
330 BOOST_CHECK( headBefore != headAfter );
331}
332
333
334// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24016
335BOOST_AUTO_TEST_CASE( RestoreCommitPreservesZipBackupsDirectory )
336{
337 LIBGIT2_SCOPE libgit;
338
339 // LOCAL_HISTORY early-exits when backups are disabled.
340 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
341 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
342 backupEnabled = true;
343
344 KI_TEST::SCOPED_TEMP_DIR tempProject( wxS( "kicad_qa_issue24016" ) );
345 const wxString& projectPath = tempProject.PathStr();
346
347 wxString boardPath =
348 projectPath + wxFileName::GetPathSeparator() + wxS( "issue24016.kicad_pcb" );
349 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108))\n" ) );
350
351 // LOCAL_HISTORY refuses to operate on directories without a project file, so seed
352 // the fixture with a minimal one to mirror a real KiCad project layout.
353 wxString projectFile =
354 projectPath + wxFileName::GetPathSeparator() + wxS( "issue24016.kicad_pro" );
355 writeTextFile( projectFile, wxS( "{}\n" ) );
356
357 LOCAL_HISTORY history;
358 BOOST_REQUIRE( history.CommitFullProjectSnapshot( projectPath, wxS( "Initial" ) ) );
359
360 wxString headHash = history.GetHeadHash( projectPath );
361 BOOST_REQUIRE( !headHash.IsEmpty() );
362
363 // Mirror SETTINGS_MANAGER::BackupProject output: a sibling "<name>-backups" directory
364 // containing one or more .zip archives.
365 wxString backupsDir =
366 projectPath + wxFileName::GetPathSeparator() + wxS( "issue24016-backups" );
367 BOOST_REQUIRE( wxFileName::Mkdir( backupsDir, 0777, wxPATH_MKDIR_FULL ) );
368
369 wxString zipPath = backupsDir + wxFileName::GetPathSeparator()
370 + wxS( "issue24016-2026-04-22_120000.zip" );
371 writeTextFile( zipPath, wxS( "pretend-zip-contents" ) );
372
373 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108) (dirty yes))\n" ) );
374
375 BOOST_REQUIRE( history.RestoreCommit( projectPath, headHash, nullptr ) );
376
377 BOOST_CHECK_MESSAGE( wxDirExists( backupsDir ),
378 "zip backups directory must survive RestoreCommit" );
379 BOOST_CHECK_MESSAGE( wxFileExists( zipPath ),
380 ".zip archive inside the backups directory must survive RestoreCommit" );
381}
382
383
397BOOST_AUTO_TEST_CASE( RestoreCommitPreservesNestedProject )
398{
399 LIBGIT2_SCOPE libgit;
400
401 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
402 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
403 backupEnabled = true;
404
405 KI_TEST::SCOPED_TEMP_DIR tempProject( wxS( "kicad_qa_nested_project" ) );
406 const wxString& projectPath = tempProject.PathStr();
407
408 // Parent project (projectA): minimal .kicad_pro plus a board file.
409 wxString parentPro = projectPath + wxFileName::GetPathSeparator() + wxS( "projectA.kicad_pro" );
410 wxString parentPcb = projectPath + wxFileName::GetPathSeparator() + wxS( "projectA.kicad_pcb" );
411 writeTextFile( parentPro, wxS( "{}\n" ) );
412 writeTextFile( parentPcb, wxS( "(kicad_pcb (version 20240108))\n" ) );
413
414 LOCAL_HISTORY history;
415 BOOST_REQUIRE( history.CommitFullProjectSnapshot( projectPath, wxS( "Initial" ) ) );
416
417 wxString headHash = history.GetHeadHash( projectPath );
418 BOOST_REQUIRE( !headHash.IsEmpty() );
419
420 // Now drop a nested project under projectA/. The user's scenario was that this nested
421 // project was added AFTER the parent's snapshot was committed - so its files are not in
422 // the restored commit, and a naive restore would propose them for deletion.
423 wxString nestedDir = projectPath + wxFileName::GetPathSeparator() + wxS( "projectB" );
424 BOOST_REQUIRE( wxFileName::Mkdir( nestedDir, 0777, wxPATH_MKDIR_FULL ) );
425
426 wxString nestedPro = nestedDir + wxFileName::GetPathSeparator() + wxS( "projectB.kicad_pro" );
427 wxString nestedPcb = nestedDir + wxFileName::GetPathSeparator() + wxS( "projectB.kicad_pcb" );
428 wxString nestedSch = nestedDir + wxFileName::GetPathSeparator() + wxS( "projectB.kicad_sch" );
429 writeTextFile( nestedPro, wxS( "{ \"nested\": true }\n" ) );
430 writeTextFile( nestedPcb, wxS( "(kicad_pcb (version 20240108) (nested yes))\n" ) );
431 writeTextFile( nestedSch, wxS( "(kicad_sch (version 20240108))\n" ) );
432
433 // Modify the parent board so the restore actually has work to do.
434 writeTextFile( parentPcb, wxS( "(kicad_pcb (version 20240108) (dirty yes))\n" ) );
435
436 BOOST_REQUIRE( history.RestoreCommit( projectPath, headHash, nullptr ) );
437
438 // The nested project's directory and every one of its files MUST survive the restore.
439 BOOST_CHECK_MESSAGE( wxDirExists( nestedDir ),
440 "nested project directory must survive RestoreCommit" );
441 BOOST_CHECK_MESSAGE( wxFileExists( nestedPro ),
442 "nested .kicad_pro must survive RestoreCommit" );
443 BOOST_CHECK_MESSAGE( wxFileExists( nestedPcb ),
444 "nested .kicad_pcb must survive RestoreCommit" );
445 BOOST_CHECK_MESSAGE( wxFileExists( nestedSch ),
446 "nested .kicad_sch must survive RestoreCommit" );
447
448 // Parent project files were correctly restored.
449 BOOST_CHECK( wxFileExists( parentPro ) );
450 BOOST_CHECK( wxFileExists( parentPcb ) );
451}
452
453
459BOOST_AUTO_TEST_CASE( RestoreCommitRetainsTimestampedBackup )
460{
461 LIBGIT2_SCOPE libgit;
462
463 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
464 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
465 backupEnabled = true;
466
467 KI_TEST::SCOPED_TEMP_DIR tempProject( wxS( "kicad_qa_retained_backup" ) );
468 const wxString& projectPath = tempProject.PathStr();
469
470 wxString boardPath = projectPath + wxFileName::GetPathSeparator() + wxS( "rb.kicad_pcb" );
471 wxString projectFile = projectPath + wxFileName::GetPathSeparator() + wxS( "rb.kicad_pro" );
472 writeTextFile( projectFile, wxS( "{}\n" ) );
473 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108))\n" ) );
474
475 LOCAL_HISTORY history;
476 BOOST_REQUIRE( history.CommitFullProjectSnapshot( projectPath, wxS( "Initial" ) ) );
477
478 wxString headHash = history.GetHeadHash( projectPath );
479 BOOST_REQUIRE( !headHash.IsEmpty() );
480
481 // Mutate the board so restore has work to do (and produces a backup).
482 writeTextFile( boardPath, wxS( "(kicad_pcb (version 20240108) (dirty yes))\n" ) );
483
484 BOOST_REQUIRE( history.RestoreCommit( projectPath, headHash, nullptr ) );
485
486 // Backups land at a SIBLING path (aProjectPath + "_restore_backup_<ts>"), so look in
487 // the parent directory of the project. Same convention as the legacy "_restore_backup".
488 wxString parentDir = wxFileName( projectPath ).GetPath();
489 wxString leafPrefix = wxFileName( projectPath ).GetFullName() + wxS( "_restore_backup_" );
490
491 wxDir dir( parentDir );
492 BOOST_REQUIRE( dir.IsOpened() );
493
494 wxString retainedBackup;
495 bool foundLegacyBackup = false;
496
497 wxString name;
498 for( bool cont = dir.GetFirst( &name, wxEmptyString, wxDIR_DIRS ); cont;
499 cont = dir.GetNext( &name ) )
500 {
501 if( name.StartsWith( leafPrefix ) )
502 {
503 retainedBackup = parentDir + wxFileName::GetPathSeparator() + name;
504
505 // No colons - Windows path-safe.
506 BOOST_CHECK_MESSAGE( name.Find( ':' ) == wxNOT_FOUND,
507 "retained backup directory name must not contain ':' "
508 "(Windows-illegal in path components)" );
509 }
510 else if( name == wxFileName( projectPath ).GetFullName() + wxS( "_restore_backup" ) )
511 {
512 foundLegacyBackup = true;
513 }
514 }
515
516 BOOST_CHECK_MESSAGE(
517 !retainedBackup.IsEmpty(),
518 "RestoreCommit must retain a timestamped _restore_backup_<ts>/ sibling directory" );
519 BOOST_CHECK_MESSAGE(
520 !foundLegacyBackup,
521 "RestoreCommit must not leave the legacy non-timestamped _restore_backup/ behind" );
522
523 // Clean up the retained backup so the test does not leak files into /tmp.
524 if( !retainedBackup.IsEmpty() && wxDirExists( retainedBackup ) )
525 wxFileName::Rmdir( retainedBackup, wxPATH_RMDIR_RECURSIVE );
526}
527
528
532BOOST_AUTO_TEST_CASE( CommitFullProjectSnapshotExcludesNonKiCadFiles )
533{
534 LIBGIT2_SCOPE libgit;
535
536 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
537 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
538 backupEnabled = true;
539
540 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_privacy" ) );
541 const wxString& path = project.PathStr();
542
543 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
544 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
545 wxS( "(kicad_pcb (version 20240108))\n" ) );
546 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_sch" ),
547 wxS( "(kicad_sch (version 20240108))\n" ) );
548
549 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "passwords.txt" ), wxS( "secret\n" ) );
550 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "datasheet.pdf" ), wxS( "fake pdf bytes\n" ) );
551 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "notes.md" ), wxS( "personal notes\n" ) );
552
553 wxString subDir = path + wxFileName::GetPathSeparator() + wxS( "docs" );
554 BOOST_REQUIRE( wxFileName::Mkdir( subDir, 0777, wxPATH_MKDIR_FULL ) );
555 writeTextFile( subDir + wxFileName::GetPathSeparator() + wxS( "manual.txt" ), wxS( "irrelevant\n" ) );
556
557 LOCAL_HISTORY history;
558 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Close" ) ) );
559
560 wxString hist = path + wxFileName::GetPathSeparator() + wxS( ".history" );
561 git_repository* repo = nullptr;
562 BOOST_REQUIRE_EQUAL( git_repository_open( &repo, hist.mb_str().data() ), 0 );
563
564 git_oid head_oid;
565 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &head_oid, repo, "HEAD" ), 0 );
566
567 git_commit* head = nullptr;
568 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &head_oid ), 0 );
569
570 git_tree* tree = nullptr;
571 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), 0 );
572
573 std::vector<std::string> committedPaths;
574 git_tree_walk(
575 tree, GIT_TREEWALK_PRE,
576 []( const char* root, const git_tree_entry* entry, void* payload ) -> int
577 {
578 auto* paths = static_cast<std::vector<std::string>*>( payload );
579
580 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
581 paths->push_back( std::string( root ) + git_tree_entry_name( entry ) );
582
583 return 0;
584 },
585 &committedPaths );
586
587 git_tree_free( tree );
588 git_commit_free( head );
589 git_repository_free( repo );
590
591 auto contains = [&]( const std::string& s )
592 {
593 return std::find( committedPaths.begin(), committedPaths.end(), s ) != committedPaths.end();
594 };
595
596 BOOST_CHECK_MESSAGE( contains( "p.kicad_pro" ), "kicad_pro must be committed" );
597 BOOST_CHECK_MESSAGE( contains( "p.kicad_pcb" ), "kicad_pcb must be committed" );
598 BOOST_CHECK_MESSAGE( contains( "p.kicad_sch" ), "kicad_sch must be committed" );
599
600 BOOST_CHECK_MESSAGE( !contains( "passwords.txt" ), "passwords.txt must NOT appear in history" );
601 BOOST_CHECK_MESSAGE( !contains( "datasheet.pdf" ), "datasheet.pdf must NOT appear in history" );
602 BOOST_CHECK_MESSAGE( !contains( "notes.md" ), "notes.md must NOT appear in history" );
603 BOOST_CHECK_MESSAGE( !contains( "docs/manual.txt" ), "subdirectory user content must NOT appear in history" );
604}
605
606
611BOOST_AUTO_TEST_CASE( FirstAutosaveSkipsCommitWhenStagedMatchesDisk )
612{
613 LIBGIT2_SCOPE libgit;
614
615 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
616 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
617 backupEnabled = true;
618
619 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_first_idle_autosave" ) );
620 const wxString& path = project.PathStr();
621
622 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
623 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
624 wxS( "(kicad_pcb (version 20240108))\n" ) );
625
626 LOCAL_HISTORY history;
627
628 // Mutating this between calls simulates the user editing the in-memory document.
629 std::string inMemoryContent = "(kicad_pcb (version 20240108))\n";
630
631 auto saver = [&inMemoryContent]( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
632 {
633 HISTORY_FILE_DATA entry;
634 entry.relativePath = wxS( "p.kicad_pcb" );
635 entry.content = inMemoryContent;
636 aFileData.push_back( std::move( entry ) );
637 };
638
639 history.RegisterSaver( &history, saver );
640
641 // Saver output matches disk, must skip (autosave path: empty tagFileType).
642 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString ) );
643 history.WaitForPendingSave();
644
645 wxString histDir = path + wxFileName::GetPathSeparator() + wxS( ".history" );
646 BOOST_CHECK( wxDirExists( histDir ) );
647
648 wxString head = history.GetHeadHash( path );
649 BOOST_CHECK_MESSAGE( head.IsEmpty(), "no untagged HEAD should exist after an idle first save" );
650
651 // Real edit, must commit.
652 inMemoryContent = "(kicad_pcb (version 20240108) (edited yes))\n";
653
654 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString ) );
655 history.WaitForPendingSave();
656
657 head = history.GetHeadHash( path );
658 BOOST_CHECK_MESSAGE( !head.IsEmpty(), "in-memory edits diverging from disk must produce a commit" );
659
660 history.UnregisterSaver( &history );
661}
662
663
670BOOST_AUTO_TEST_CASE( SaverSkippedAfterOwningDocumentDestroyed )
671{
672 LIBGIT2_SCOPE libgit;
673
674 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
675 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
676 backupEnabled = true;
677
678 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_saver_lifetime" ) );
679 const wxString& path = project.PathStr();
680 const wxString sep = wxFileName::GetPathSeparator();
681
682 writeTextFile( path + sep + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
683 writeTextFile( path + sep + wxS( "p.kicad_pcb" ), wxS( "(kicad_pcb (version 20240108))\n" ) );
684
685 LOCAL_HISTORY history;
686
687 // The saver only touches this heap counter, so it stays safe to invoke after the board is gone;
688 // gating it on the board's token is the behaviour under test.
689 auto runCount = std::make_shared<int>( 0 );
690 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
691
692 history.RegisterSaver( board.get(),
693 [runCount]( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
694 {
695 ++( *runCount );
696
697 HISTORY_FILE_DATA entry;
698 entry.relativePath = wxS( "p.kicad_pcb" );
699 entry.content = "(kicad_pcb (version 20240108) (edited yes))\n";
700 aFileData.push_back( std::move( entry ) );
701 },
702 board->GetHistoryLifetimeToken() );
703
704 // Positive control: while the board is alive the saver runs.
705 history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString );
706 history.WaitForPendingSave();
707 BOOST_CHECK_EQUAL( *runCount, 1 );
708
709 // Destroy the document; its expired token must make both runners skip and drop the saver.
710 board.reset();
711
712 history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString );
713 history.WaitForPendingSave();
714 BOOST_CHECK_MESSAGE( *runCount == 1, "commit runner invoked a saver whose board was destroyed" );
715
717 BOOST_CHECK_MESSAGE( *runCount == 1, "autosave-file runner invoked a saver whose board was destroyed" );
718}
719
720
726BOOST_AUTO_TEST_CASE( FirstManualSaveAlwaysCommitsOnFreshProject )
727{
728 LIBGIT2_SCOPE libgit;
729
730 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
731 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
732 backupEnabled = true;
733
734 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_first_manual_save" ) );
735 const wxString& path = project.PathStr();
736
737 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
738 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
739 wxS( "(kicad_pcb (version 20240108))\n" ) );
740
741 LOCAL_HISTORY history;
742
743 std::string inMemoryContent = "(kicad_pcb (version 20240108))\n";
744
745 auto saver = [&inMemoryContent]( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
746 {
747 HISTORY_FILE_DATA entry;
748 entry.relativePath = wxS( "p.kicad_pcb" );
749 entry.content = inMemoryContent;
750 aFileData.push_back( std::move( entry ) );
751 };
752
753 history.RegisterSaver( &history, saver );
754
755 // Manual save (non-empty tagFileType) on fresh project: commit even when staged matches disk.
756 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Manual Save" ), wxS( "pcb" ) ) );
757
758 wxString head = history.GetHeadHash( path );
759 BOOST_CHECK_MESSAGE( !head.IsEmpty(), "manual save on a fresh project must commit even when staged matches disk" );
760
761 history.UnregisterSaver( &history );
762}
763
764
765// With backups enabled but the format set to Zip, autosave uses legacy recovery files, so the
766// incremental git-commit path must be a no-op rather than extending a history the user switched
767// off (issue 24773).
768BOOST_AUTO_TEST_CASE( ZipFormatSkipsIncrementalAutosave )
769{
770 LIBGIT2_SCOPE libgit;
771
772 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
773 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
774 backupEnabled = true;
775
777 SCOPED_BACKUP_FORMAT_OVERRIDE restoreFormat( format );
778 format = BACKUP_FORMAT::ZIP;
779
780 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_zip_skips_incremental" ) );
781 const wxString& path = project.PathStr();
782
783 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
784 writeTextFile( path + wxFileName::GetPathSeparator() + wxS( "p.kicad_pcb" ),
785 wxS( "(kicad_pcb (version 20240108))\n" ) );
786
787 LOCAL_HISTORY history;
788
789 auto saver = []( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
790 {
791 HISTORY_FILE_DATA entry;
792 entry.relativePath = wxS( "p.kicad_pcb" );
793 entry.content = "(kicad_pcb (version 20240108) (edited yes))\n";
794 aFileData.push_back( std::move( entry ) );
795 };
796
797 history.RegisterSaver( &history, saver );
798
799 BOOST_REQUIRE( history.RunRegisteredSaversAndCommit( path, wxS( "Autosave" ), wxEmptyString ) );
800 history.WaitForPendingSave();
801
802 BOOST_CHECK_MESSAGE( history.GetHeadHash( path ).IsEmpty(),
803 "zip backup format must not create incremental autosave commits" );
804
805 history.UnregisterSaver( &history );
806}
807
808
809// The Zip backup format must reliably write legacy recovery files on autosave so a crash does
810// not lose work between manual saves (issue 24773).
811BOOST_AUTO_TEST_CASE( ZipFormatWritesRecoveryFiles )
812{
813 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
814 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
815 backupEnabled = true;
816
818 SCOPED_BACKUP_FORMAT_OVERRIDE restoreFormat( format );
819 format = BACKUP_FORMAT::ZIP;
820
822 SCOPED_BACKUP_LOCATION_OVERRIDE restoreLocation( location );
824
825 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_zip_recovery_files" ) );
826 const wxString& path = project.PathStr();
827 const wxString sep = wxFileName::GetPathSeparator();
828
829 writeTextFile( path + sep + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
830
832 SCOPED_PROJECT_LOAD loadedProject( mgr, path + sep + wxS( "p.kicad_pro" ) );
833
834 LOCAL_HISTORY history;
835
836 auto saver = []( const wxString&, std::vector<HISTORY_FILE_DATA>& aFileData )
837 {
838 HISTORY_FILE_DATA entry;
839 entry.relativePath = wxS( "p.kicad_pcb" );
840 entry.content = "(kicad_pcb (version 20240108) (edited yes))\n";
841 aFileData.push_back( std::move( entry ) );
842 };
843
844 history.RegisterSaver( &history, saver );
845
847
848 wxString autosavePath = path + sep + wxS( "_autosave-p.kicad_pcb" );
849 BOOST_CHECK_MESSAGE( wxFileExists( autosavePath ),
850 "zip backup format must write autosave recovery files" );
851
852 history.UnregisterSaver( &history );
853}
854
855
856// A content-identical autosave with a newer mtime (cloud-sync touch) must not be flagged
857// stale, or recovery prompts fire on every open with nothing to restore (issue 24126).
858BOOST_AUTO_TEST_CASE( CloudSyncTouchedAutosaveFalselyFlaggedStale )
859{
860 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
861 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
862 backupEnabled = true;
863
865 SCOPED_BACKUP_LOCATION_OVERRIDE restoreLocation( location );
867
868 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_cloudsync_autosave" ) );
869 const wxString& path = project.PathStr();
870 const wxString sep = wxFileName::GetPathSeparator();
871
872 // FindStaleAutosaveFiles resolves the autosave root through the active project, so it must
873 // be loaded; the .kicad_pro must exist on disk first for LoadProject to succeed.
874 writeTextFile( path + sep + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
875
877 SCOPED_PROJECT_LOAD loadedProject( mgr, path + sep + wxS( "p.kicad_pro" ) );
878
879 const wxString sourcePath = path + sep + wxS( "p.kicad_pcb" );
880 const wxString autosavePath = path + sep + wxS( "_autosave-p.kicad_pcb" );
881 const wxString boardContent = wxS( "(kicad_pcb (version 20240108))\n" );
882
883 // Byte-identical source and "_autosave-" companion, mirroring a clean save.
884 writeTextFile( sourcePath, boardContent );
885 writeTextFile( autosavePath, boardContent );
886
887 LOCAL_HISTORY history;
888
889 // Cloud-sync touch gives the identical autosave a strictly newer mtime.
890 wxDateTime srcMtime = wxFileName( sourcePath ).GetModificationTime();
891 wxDateTime newerMtime = srcMtime + wxTimeSpan::Seconds( 60 );
892 BOOST_REQUIRE( wxFileName( autosavePath ).SetTimes( &newerMtime, &newerMtime, nullptr ) );
893
894 std::vector<wxString> exts{ wxS( "kicad_pcb" ) };
895 auto stale = history.FindStaleAutosaveFiles( path, exts );
896
897 BOOST_CHECK_MESSAGE( stale.empty(),
898 "Content-identical autosave with newer mtime was flagged stale, "
899 "triggering a spurious recovery prompt (issue 24126)" );
900}
901
902
903// Counterpart to the above: an autosave whose content genuinely differs (a real unsaved edit)
904// must still be flagged stale so recovery continues to fire.
905BOOST_AUTO_TEST_CASE( DivergentAutosaveStillFlaggedStale )
906{
907 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
908 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
909 backupEnabled = true;
910
912 SCOPED_BACKUP_LOCATION_OVERRIDE restoreLocation( location );
914
915 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_divergent_autosave" ) );
916 const wxString& path = project.PathStr();
917 const wxString sep = wxFileName::GetPathSeparator();
918
919 writeTextFile( path + sep + wxS( "p.kicad_pro" ), wxS( "{}\n" ) );
920
922 SCOPED_PROJECT_LOAD loadedProject( mgr, path + sep + wxS( "p.kicad_pro" ) );
923
924 const wxString sourcePath = path + sep + wxS( "p.kicad_pcb" );
925 const wxString autosavePath = path + sep + wxS( "_autosave-p.kicad_pcb" );
926
927 // Autosave content diverges from the source, a genuine unsaved edit to recover.
928 writeTextFile( sourcePath, wxS( "(kicad_pcb (version 20240108))\n" ) );
929 writeTextFile( autosavePath, wxS( "(kicad_pcb (version 20240108) (edited yes))\n" ) );
930
931 LOCAL_HISTORY history;
932
933 wxDateTime srcMtime = wxFileName( sourcePath ).GetModificationTime();
934 wxDateTime newerMtime = srcMtime + wxTimeSpan::Seconds( 60 );
935 BOOST_REQUIRE( wxFileName( autosavePath ).SetTimes( &newerMtime, &newerMtime, nullptr ) );
936
937 std::vector<wxString> exts{ wxS( "kicad_pcb" ) };
938 auto stale = history.FindStaleAutosaveFiles( path, exts );
939
940 BOOST_CHECK_MESSAGE( stale.size() == 1,
941 "Genuinely divergent autosave must still be flagged stale for recovery" );
942}
943
944
945BOOST_AUTO_TEST_CASE( EnforceSizeLimitKeepsUnstagedFilesInNextSnapshot )
946{
947 LIBGIT2_SCOPE libgit;
948
949 bool& backupEnabled = Pgm().GetCommonSettings()->m_Backup.enabled;
950 SCOPED_BOOL_OVERRIDE restoreBackupFlag( backupEnabled );
951 backupEnabled = true;
952
953 KI_TEST::SCOPED_TEMP_DIR project( wxS( "kicad_qa_trim_partial_save" ) );
954 const wxString& path = project.PathStr();
955 const wxString sep = wxFileName::GetPathSeparator();
956
957 writeTextFile( path + sep + wxS( "trim.kicad_pro" ), wxS( "{}\n" ) );
958 writeTextFile( path + sep + wxS( "trim.kicad_pcb" ), wxS( "(kicad_pcb (version 20240108))\n" ) );
959 writeTextFile( path + sep + wxS( "trim.kicad_sch" ), wxS( "(kicad_sch (version 20240108))\n" ) );
960
961 LOCAL_HISTORY history;
962 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Initial" ) ) );
963
964 writeTextFile( path + sep + wxS( "trim.kicad_pcb" ), wxS( "(kicad_pcb (version 20240108) (net 1))\n" ) );
965 BOOST_REQUIRE( history.CommitFullProjectSnapshot( path, wxS( "Second" ) ) );
966
967 BOOST_REQUIRE( history.EnforceSizeLimit( path, 1 ) );
968
969 writeTextFile( path + sep + wxS( "trim.kicad_sch" ), wxS( "(kicad_sch (version 20240108) (mod))\n" ) );
970 BOOST_REQUIRE( history.CommitSnapshot( { path + sep + wxS( "trim.kicad_sch" ) }, wxS( "Sch Save" ) ) );
971
972 wxString historyDir = path + sep + wxS( ".history" );
973 git_repository* repo = nullptr;
974 BOOST_REQUIRE( git_repository_open( &repo, historyDir.mb_str().data() ) == 0 );
975
976 git_oid headOid;
977 git_commit* head = nullptr;
978 git_tree* tree = nullptr;
979 BOOST_REQUIRE( git_reference_name_to_id( &headOid, repo, "HEAD" ) == 0 );
980 BOOST_REQUIRE( git_commit_lookup( &head, repo, &headOid ) == 0 );
981 BOOST_REQUIRE( git_commit_tree( &tree, head ) == 0 );
982
983 BOOST_CHECK_MESSAGE( git_tree_entry_byname( tree, "trim.kicad_sch" ),
984 "schematic missing from snapshot after trim" );
985 BOOST_CHECK_MESSAGE( git_tree_entry_byname( tree, "trim.kicad_pcb" ), "board dropped from snapshot after trim" );
986 BOOST_CHECK_MESSAGE( git_tree_entry_byname( tree, "trim.kicad_pro" ),
987 "project file dropped from snapshot after trim" );
988
989 git_tree_free( tree );
990 git_commit_free( head );
991 git_repository_free( repo );
992}
993
994
995// A snapshot must serialize the live project settings, not copy the stale file from disk.
996BOOST_AUTO_TEST_CASE( ProjectSnapshotCarriesUnsavedSettings )
997{
998 SNAPSHOT_PROJECT tc( wxS( "kicad_qa_pro_snapshot" ), wxS( "snap.kicad_pro" ) );
999 BOOST_REQUIRE( tc.m_mgr.SaveProject() );
1000
1001 BOARD_DESIGN_SETTINGS bds( &tc.ProjectFile(), "board.design_settings" );
1002 addViaStackPreset( bds, wxS( "History Preset" ) );
1003
1004 std::string bytes = snapshotProjectBytes( tc.Project() );
1005
1006 BOOST_CHECK_MESSAGE( bytes.find( "History Preset" ) != std::string::npos,
1007 "project file snapshot must carry the unsaved via stack preset" );
1008}
1009
1010
1011// The snapshot flush must not swallow the next real save.
1012BOOST_AUTO_TEST_CASE( ProjectSaveStillWritesAfterSnapshot )
1013{
1014 SNAPSHOT_PROJECT tc( wxS( "kicad_qa_pro_snapshot_save" ), wxS( "snapsave.kicad_pro" ) );
1015 BOOST_REQUIRE( tc.m_mgr.SaveProject() );
1016
1017 PROJECT_FILE& projectFile = tc.ProjectFile();
1018 BOARD_DESIGN_SETTINGS bds( &projectFile, "board.design_settings" );
1019 addViaStackPreset( bds, wxS( "History Preset" ) );
1020
1021 projectFile.m_TextVars[wxS( "HISTORY_VAR" )] = wxS( "kept" );
1022
1023 snapshotProjectBytes( tc.Project() );
1024
1025 BOOST_CHECK_MESSAGE( projectFile.SaveToFile( tc.m_tempDir.PathStr() ),
1026 "a real save after the snapshot must still write the file" );
1027
1028 std::string onDisk = readTextFile( tc.m_proPath );
1029
1030 BOOST_CHECK_MESSAGE( onDisk.find( "History Preset" ) != std::string::npos,
1031 "the via stack preset must reach the file after the snapshot" );
1032 BOOST_CHECK_MESSAGE( onDisk.find( "HISTORY_VAR" ) != std::string::npos,
1033 "the text variable must reach the file after the snapshot" );
1034}
1035
1036
1037// A snapshot of a clean project must not make the next save rewrite an unchanged file (#24402).
1038BOOST_AUTO_TEST_CASE( ProjectSnapshotDoesNotDirtyACleanProject )
1039{
1040 SNAPSHOT_PROJECT tc( wxS( "kicad_qa_pro_snapshot_clean" ), wxS( "snapclean.kicad_pro" ) );
1041
1042 PROJECT_FILE& projectFile = tc.ProjectFile();
1043 BOARD_DESIGN_SETTINGS bds( &projectFile, "board.design_settings" );
1044
1045 BOOST_REQUIRE( tc.m_mgr.SaveProject() );
1046
1047 std::string before = readTextFile( tc.m_proPath );
1048
1049 snapshotProjectBytes( tc.Project() );
1050
1051 BOOST_CHECK_MESSAGE( !projectFile.SaveToFile( tc.m_tempDir.PathStr() ),
1052 "an unchanged project must not be rewritten after a snapshot" );
1053 BOOST_CHECK_EQUAL( before, readTextFile( tc.m_proPath ) );
1054}
1055
1056
const char * name
Container for design settings for a BOARD object.
std::vector< VIA_STACK_PRESET > m_ViaStackPresets
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:410
void SaveToHistory(const wxString &aProjectPath, std::vector< HISTORY_FILE_DATA > &aFileData)
Serialize board into HISTORY_FILE_DATA for non-blocking history commit.
Definition board.cpp:4531
void SetProject(PROJECT *aProject, bool aReferenceOnly=false)
Link a board to a given project.
Definition board.cpp:377
const wxString & GetFileName() const
Definition board.h:453
void ClearProject()
Definition board.cpp:418
PROJECT * GetProject() const
Definition board.h:775
AUTO_BACKUP m_Backup
wxString PathStr() const
Get the path to the temporary directory as a wxString.
Definition file_utils.h:62
Simple local history manager built on libgit2.
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.
void WaitForPendingSave()
Block until any pending background save completes.
bool RestoreCommit(const wxString &aProjectPath, const wxString &aHash, wxWindow *aParent=nullptr, bool aConfirm=true)
Restore the project files to the state recorded by the given commit hash.
bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
bool CommitSnapshot(const std::vector< wxString > &aFiles, const wxString &aTitle)
Commit the given files to the local history repository.
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...
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> &aSaver, const std::weak_ptr< void > &aLifetime={})
Register a saver callback invoked during autosave history commits.
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:546
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:123
The backing store for a PROJECT, in JSON format.
std::map< wxString, wxString > m_TextVars
bool SaveToFile(const wxString &aDirectory="", bool aForce=false) override
Calls Store() and then writes the contents of the JSON document to a file.
Container for project specific data.
Definition project.h:63
virtual const wxString GetProjectFullName() const
Return the full path and name of the project.
Definition project.cpp:177
virtual const wxString GetProjectPath() const
Return the full path of the project.
Definition project.cpp:183
void SaveToHistory(const wxString &aProjectPath, std::vector< HISTORY_FILE_DATA > &aFileData)
Produce HISTORY_FILE_DATA entries for project files (.kicad_pro and .kicad_prl).
Definition project.cpp:467
bool SaveProject(const wxString &aFullPath=wxEmptyString, PROJECT *aProject=nullptr)
Save a loaded 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.
BACKUP_FORMAT
@ ZIP
Zip archive snapshots; autosave uses recovery files.
BACKUP_LOCATION
@ PROJECT_DIR
Inside the project directory (default)
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
BACKUP_LOCATION location
Where backups, history, and autosave files live.
BACKUP_FORMAT format
Backup format (incremental git history vs zip archives)
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...
wxString sourcePath
For file-copy savers (small files like .kicad_pro)
std::string content
Serialized content (mutually exclusive with sourcePath)
wxString relativePath
Destination path relative to the project root.
A named microvia stack definition, chosen while routing instead of entering the values each time.
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.
std::string path
VECTOR2I location
BOOST_CHECK_EQUAL(result, "25.4")