KiCad PCB EDA Suite
Loading...
Searching...
No Matches
local_history.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, you may find one here:
18 * http://www.gnu.org/licenses/gpl-3.0.html
19 * or you may search the http://www.gnu.org website for the version 3 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include <local_history.h>
26#include <history_lock.h>
28#include <lockfile.h>
30#include <pgm_base.h>
31#include <thread_pool.h>
32#include <trace_helpers.h>
34#include <confirm.h>
35
36#include <git2.h>
37#include <wx/filename.h>
38#include <wx/filefn.h>
39#include <wx/ffile.h>
40#include <wx/dir.h>
41#include <wx/datetime.h>
42#include <wx/log.h>
43#include <wx/msgdlg.h>
44
45#include <vector>
46#include <string>
47#include <memory>
48#include <algorithm>
49#include <set>
50#include <map>
51#include <functional>
52#include <cstring>
53
54static wxString historyPath( const wxString& aProjectPath )
55{
56 wxFileName p( aProjectPath, wxEmptyString );
57 p.AppendDir( wxS( ".history" ) );
58 return p.GetPath();
59}
60
64
69
70void LOCAL_HISTORY::NoteFileChange( const wxString& aFile )
71{
72 wxFileName fn( aFile );
73
74 if( fn.GetFullName() == wxS( "fp-info-cache" ) || !Pgm().GetCommonSettings()->m_Backup.enabled )
75 return;
76
77 m_pendingFiles.insert( fn.GetFullPath() );
78}
79
80
82 const void* aSaverObject,
83 const std::function<void( const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
84{
85 if( m_savers.find( aSaverObject ) != m_savers.end() )
86 {
87 wxLogTrace( traceAutoSave, wxS("[history] Saver %p already registered, skipping"), aSaverObject );
88 return;
89 }
90
91 m_savers[aSaverObject] = aSaver;
92 wxLogTrace( traceAutoSave, wxS("[history] Registered saver %p (total=%zu)"), aSaverObject, m_savers.size() );
93}
94
95
96void LOCAL_HISTORY::UnregisterSaver( const void* aSaverObject )
97{
99
100 auto it = m_savers.find( aSaverObject );
101
102 if( it != m_savers.end() )
103 {
104 m_savers.erase( it );
105 wxLogTrace( traceAutoSave, wxS("[history] Unregistered saver %p (total=%zu)"), aSaverObject, m_savers.size() );
106 }
107}
108
109
111{
113 m_savers.clear();
114 wxLogTrace( traceAutoSave, wxS("[history] Cleared all savers") );
115}
116
117
118bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath, const wxString& aTitle )
119{
120 if( !Pgm().GetCommonSettings()->m_Backup.enabled )
121 {
122 wxLogTrace( traceAutoSave, wxS("Autosave disabled, returning" ) );
123 return true;
124 }
125
126 wxLogTrace( traceAutoSave, wxS("[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu"),
127 aProjectPath, aTitle, m_savers.size() );
128
129 if( m_savers.empty() )
130 {
131 wxLogTrace( traceAutoSave, wxS("[history] no savers registered; skipping") );
132 return false;
133 }
134
135 // Skip if previous background save is still running
136 if( m_saveInProgress.load( std::memory_order_acquire ) )
137 {
138 wxLogTrace( traceAutoSave, wxS("[history] previous save still in progress; skipping cycle") );
139 return false;
140 }
141
142 // Phase 1 (UI thread): call savers to collect serialized data
143 std::vector<HISTORY_FILE_DATA> fileData;
144
145 for( const auto& [saverObject, saver] : m_savers )
146 {
147 size_t before = fileData.size();
148 saver( aProjectPath, fileData );
149 wxLogTrace( traceAutoSave, wxS("[history] saver %p produced %zu entries (total=%zu)"),
150 saverObject, fileData.size() - before, fileData.size() );
151 }
152
153 // Filter out any entries not within the project directory
154 wxString projectDir = aProjectPath;
155 if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
156 projectDir += wxFileName::GetPathSeparator();
157
158 auto it = std::remove_if( fileData.begin(), fileData.end(),
159 [&projectDir]( const HISTORY_FILE_DATA& entry )
160 {
161 if( !entry.path.StartsWith( projectDir ) )
162 {
163 wxLogTrace( traceAutoSave, wxS("[history] filtered out entry outside project: %s"), entry.path );
164 return true;
165 }
166 return false;
167 } );
168 fileData.erase( it, fileData.end() );
169
170 if( fileData.empty() )
171 {
172 wxLogTrace( traceAutoSave, wxS("[history] saver set produced no entries; skipping") );
173 return false;
174 }
175
176 // Phase 2: submit Prettify + file I/O + git to background thread
177 m_saveInProgress.store( true, std::memory_order_release );
178
179 m_pendingFuture = GetKiCadThreadPool().submit_task(
180 [this, projectPath = aProjectPath, title = aTitle,
181 data = std::move( fileData )]() mutable -> bool
182 {
183 bool result = commitInBackground( projectPath, title, data );
184 m_saveInProgress.store( false, std::memory_order_release );
185 return result;
186 } );
187
188 return true;
189}
190
191
192bool LOCAL_HISTORY::commitInBackground( const wxString& aProjectPath, const wxString& aTitle,
193 const std::vector<HISTORY_FILE_DATA>& aFileData )
194{
195 wxLogTrace( traceAutoSave, wxS("[history] background: writing %zu entries for '%s'"),
196 aFileData.size(), aProjectPath );
197
198 wxString hist = historyPath( aProjectPath );
199
200 // Write files to the .history mirror
201 for( const HISTORY_FILE_DATA& entry : aFileData )
202 {
203 if( !entry.content.empty() )
204 {
205 std::string buf = entry.content;
206
207 if( entry.prettify )
208 KICAD_FORMAT::Prettify( buf, entry.formatMode );
209
210 wxFFile fp( entry.path, wxS( "wb" ) );
211
212 if( fp.IsOpened() )
213 {
214 fp.Write( buf.data(), buf.size() );
215 fp.Close();
216 wxLogTrace( traceAutoSave, wxS("[history] background: wrote %zu bytes to '%s'"),
217 buf.size(), entry.path );
218 }
219 else
220 {
221 wxLogTrace( traceAutoSave, wxS("[history] background: failed to open '%s' for writing"),
222 entry.path );
223 }
224 }
225 else if( !entry.sourcePath.IsEmpty() )
226 {
227 wxCopyFile( entry.sourcePath, entry.path, true );
228 wxLogTrace( traceAutoSave, wxS("[history] background: copied '%s' -> '%s'"),
229 entry.sourcePath, entry.path );
230 }
231 }
232
233 // Acquire locks using hybrid locking strategy
234 HISTORY_LOCK_MANAGER lock( aProjectPath );
235
236 if( !lock.IsLocked() )
237 {
238 wxLogTrace( traceAutoSave, wxS("[history] background: failed to acquire lock: %s"), lock.GetLockError() );
239 return false;
240 }
241
242 git_repository* repo = lock.GetRepository();
243 git_index* index = lock.GetIndex();
244
245 // Stage all written files
246 for( const HISTORY_FILE_DATA& entry : aFileData )
247 {
248 wxFileName src( entry.path );
249
250 if( !src.FileExists() )
251 continue;
252
253 if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
254 {
255 std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
256 git_index_add_bypath( index, relHist.c_str() );
257 }
258 }
259
260 // Compare index to HEAD; if no diff -> abort to avoid empty commit.
261 git_oid head_oid;
262 git_commit* head_commit = nullptr;
263 git_tree* head_tree = nullptr;
264
265 bool headExists = ( git_reference_name_to_id( &head_oid, repo, "HEAD" ) == 0 )
266 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
267 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
268
269 git_tree* rawIndexTree = nullptr;
270 git_oid index_tree_oid;
271
272 if( git_index_write_tree( &index_tree_oid, index ) != 0 )
273 {
274 if( head_tree ) git_tree_free( head_tree );
275 if( head_commit ) git_commit_free( head_commit );
276 wxLogTrace( traceAutoSave, wxS("[history] background: failed to write index tree" ) );
277 return false;
278 }
279
280 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
281 std::unique_ptr<git_tree, decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
282
283 bool hasChanges = true;
284
285 if( headExists )
286 {
287 git_diff* diff = nullptr;
288
289 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(), nullptr ) == 0 )
290 {
291 hasChanges = git_diff_num_deltas( diff ) > 0;
292 wxLogTrace( traceAutoSave, wxS("[history] background: diff deltas=%u"), (unsigned) git_diff_num_deltas( diff ) );
293 git_diff_free( diff );
294 }
295 }
296
297 if( head_tree ) git_tree_free( head_tree );
298 if( head_commit ) git_commit_free( head_commit );
299
300 if( !hasChanges )
301 {
302 wxLogTrace( traceAutoSave, wxS("[history] background: no changes detected; no commit") );
303 return false; // Nothing new; skip commit.
304 }
305
306 git_signature* rawSig = nullptr;
307 git_signature_now( &rawSig, "KiCad", "[email protected]" );
308 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
309
310 git_commit* parent = nullptr;
311 git_oid parent_id;
312 int parents = 0;
313
314 if( git_reference_name_to_id( &parent_id, repo, "HEAD" ) == 0 )
315 {
316 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
317 parents = 1;
318 }
319
320 wxString msg = aTitle.IsEmpty() ? wxString( "Autosave" ) : aTitle;
321 git_oid commit_id;
322 const git_commit* constParent = parent;
323
324 int rc = git_commit_create( &commit_id, repo, "HEAD", sig.get(), sig.get(), nullptr,
325 msg.mb_str().data(), indexTree.get(), parents,
326 parents ? &constParent : nullptr );
327
328 if( rc == 0 )
329 wxLogTrace( traceAutoSave, wxS("[history] background: commit created %s (%s entries=%zu)"),
330 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
331 else
332 wxLogTrace( traceAutoSave, wxS("[history] background: commit failed rc=%d"), rc );
333
334 if( parent ) git_commit_free( parent );
335
336 git_index_write( index );
337 return rc == 0;
338}
339
340
342{
343 if( m_pendingFuture.valid() )
344 {
345 wxLogTrace( traceAutoSave, wxS("[history] waiting for pending background save") );
346 m_pendingFuture.get();
347 }
348}
349
350
352{
353 std::vector<wxString> files( m_pendingFiles.begin(), m_pendingFiles.end() );
354 m_pendingFiles.clear();
355 return CommitSnapshot( files, wxS( "Autosave" ) );
356}
357
358
359bool LOCAL_HISTORY::Init( const wxString& aProjectPath )
360{
361 if( aProjectPath.IsEmpty() )
362 return false;
363
364 if( !Pgm().GetCommonSettings()->m_Backup.enabled )
365 return true;
366
367 wxString hist = historyPath( aProjectPath );
368
369 if( !wxDirExists( hist ) )
370 {
371 if( wxIsWritable( aProjectPath ) )
372 {
373 if( !wxMkdir( hist ) )
374 {
375 return false;
376 }
377 }
378 }
379
380 git_repository* rawRepo = nullptr;
381 bool isNewRepo = false;
382
383 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
384 {
385 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
386 return false;
387
388 isNewRepo = true;
389
390 wxFileName ignoreFile( hist, wxS( ".gitignore" ) );
391 if( !ignoreFile.FileExists() )
392 {
393 wxFFile f( ignoreFile.GetFullPath(), wxT( "w" ) );
394 if( f.IsOpened() )
395 {
396 f.Write( wxS( "fp-info-cache\n*-backups\nREADME.txt\n" ) );
397 f.Close();
398 }
399 }
400
401 wxFileName readmeFile( hist, wxS( "README.txt" ) );
402
403 if( !readmeFile.FileExists() )
404 {
405 wxFFile f( readmeFile.GetFullPath(), wxT( "w" ) );
406
407 if( f.IsOpened() )
408 {
409 f.Write( wxS( "KiCad Local History Directory\n"
410 "=============================\n\n"
411 "This directory contains automatic snapshots of your project files.\n"
412 "KiCad periodically saves copies of your work here, allowing you to\n"
413 "recover from accidental changes or data loss.\n\n"
414 "You can browse and restore previous versions through KiCad's\n"
415 "File > Local History menu.\n\n"
416 "To disable this feature:\n"
417 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
418 "This directory can be safely deleted if you no longer need the\n"
419 "history, but doing so will permanently remove all saved snapshots.\n" ) );
420 f.Close();
421 }
422 }
423 }
424
425 git_repository_free( rawRepo );
426
427 // If this is a newly initialized repository with no commits, create an initial snapshot
428 // of all existing project files
429 if( isNewRepo )
430 {
431 wxLogTrace( traceAutoSave, wxS( "[history] Init: New repository created, collecting existing files" ) );
432
433 // Collect all files in the project directory (excluding backups and hidden files)
434 wxArrayString files;
435 std::function<void( const wxString& )> collect = [&]( const wxString& path )
436 {
437 wxString name;
438 wxDir d( path );
439
440 if( !d.IsOpened() )
441 return;
442
443 bool cont = d.GetFirst( &name );
444
445 while( cont )
446 {
447 // Skip hidden files/directories and backup directories
448 if( name.StartsWith( wxS( "." ) ) || name.EndsWith( wxS( "-backups" ) ) )
449 {
450 cont = d.GetNext( &name );
451 continue;
452 }
453
454 wxFileName fn( path, name );
455 wxString fullPath = fn.GetFullPath();
456
457 if( wxFileName::DirExists( fullPath ) )
458 {
459 collect( fullPath );
460 }
461 else if( fn.FileExists() )
462 {
463 // Skip transient files
464 if( fn.GetFullName() != wxS( "fp-info-cache" ) )
465 files.Add( fn.GetFullPath() );
466 }
467
468 cont = d.GetNext( &name );
469 }
470 };
471
472 collect( aProjectPath );
473
474 if( files.GetCount() > 0 )
475 {
476 std::vector<wxString> vec;
477 vec.reserve( files.GetCount() );
478
479 for( unsigned i = 0; i < files.GetCount(); ++i )
480 vec.push_back( files[i] );
481
482 wxLogTrace( traceAutoSave, wxS( "[history] Init: Creating initial snapshot with %zu files" ), vec.size() );
483 CommitSnapshot( vec, wxS( "Initial snapshot" ) );
484
485 // Tag the initial snapshot as saved so HeadNewerThanLastSave() doesn't
486 // incorrectly offer to restore when the project is first opened
487 TagSave( aProjectPath, wxS( "project" ) );
488 }
489 else
490 {
491 wxLogTrace( traceAutoSave, wxS( "[history] Init: No files found to add to initial snapshot" ) );
492 }
493 }
494
495 return true;
496}
497
498
499// Helper function to commit files using an already-acquired lock
506
507
508static SNAPSHOT_COMMIT_RESULT commitSnapshotWithLock( git_repository* repo, git_index* index,
509 const wxString& aHistoryPath, const wxString& aProjectPath,
510 const std::vector<wxString>& aFiles, const wxString& aTitle )
511{
512 std::vector<std::string> filesArrStr;
513
514 for( const wxString& file : aFiles )
515 {
516 wxFileName src( file );
517 wxString relPath;
518
519 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
520 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
521 else
522 relPath = src.GetFullName(); // Fallback (should not normally happen)
523
524 relPath.Replace( "\\", "/" ); // libgit2 needs forward slashes on all platforms
525 std::string relPathStr = relPath.ToStdString();
526
527 unsigned int status = 0;
528 int rc = git_status_file( &status, repo, relPathStr.data() );
529
530 if( rc == 0 && status != 0 )
531 {
532 wxLogTrace( traceAutoSave, wxS( "File %s status %d " ), relPath, status );
533 filesArrStr.emplace_back( relPathStr );
534 }
535 else if( rc != 0 )
536 {
537 wxLogTrace( traceAutoSave, wxS( "File %s status error %d " ), relPath, rc );
538 filesArrStr.emplace_back( relPathStr ); // Add anyway even if the file is untracked.
539 }
540 }
541
542 std::vector<char*> cStrings( filesArrStr.size() );
543
544 for( size_t i = 0; i < filesArrStr.size(); i++ )
545 cStrings[i] = filesArrStr[i].data();
546
547 git_strarray filesArrGit;
548 filesArrGit.count = filesArrStr.size();
549 filesArrGit.strings = cStrings.data();
550
551 if( filesArrStr.size() == 0 )
552 {
553 wxLogTrace( traceAutoSave, wxS( "No changes, skipping" ) );
555 }
556
557 int rc = git_index_add_all( index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
558 NULL );
559 wxLogTrace( traceAutoSave, wxS( "Adding %zu files, rc %d" ), filesArrStr.size(), rc );
560
561 if( rc != 0 )
563
564 git_oid tree_id;
565 if( git_index_write_tree( &tree_id, index ) != 0 )
567
568 git_tree* rawTree = nullptr;
569 git_tree_lookup( &rawTree, repo, &tree_id );
570 std::unique_ptr<git_tree, decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
571
572 git_signature* rawSig = nullptr;
573 git_signature_now( &rawSig, "KiCad", "[email protected]" );
574 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( rawSig,
575 &git_signature_free );
576
577 git_commit* rawParent = nullptr;
578 git_oid parent_id;
579 int parents = 0;
580
581 if( git_reference_name_to_id( &parent_id, repo, "HEAD" ) == 0 )
582 {
583 git_commit_lookup( &rawParent, repo, &parent_id );
584 parents = 1;
585 }
586
587 std::unique_ptr<git_commit, decltype( &git_commit_free )> parent( rawParent,
588 &git_commit_free );
589
590 git_tree* rawParentTree = nullptr;
591
592 if( parent )
593 git_commit_tree( &rawParentTree, parent.get() );
594
595 std::unique_ptr<git_tree, decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
596
597 git_diff* rawDiff = nullptr;
598 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(), index, nullptr );
599 std::unique_ptr<git_diff, decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
600
601 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
602
603 if( numChangedFiles == 0 )
604 {
605 wxLogTrace( traceAutoSave, wxS( "No actual changes in tree, skipping commit" ) );
607 }
608
609 wxString msg;
610
611 if( !aTitle.IsEmpty() )
612 msg << aTitle << wxS( ": " );
613
614 msg << numChangedFiles << wxS( " files changed" );
615
616 for( size_t i = 0; i < numChangedFiles; ++i )
617 {
618 const git_diff_delta* delta = git_diff_get_delta( diff.get(), i );
619 git_patch* rawPatch = nullptr;
620 git_patch_from_diff( &rawPatch, diff.get(), i );
621 std::unique_ptr<git_patch, decltype( &git_patch_free )> patch( rawPatch,
622 &git_patch_free );
623 size_t context = 0, adds = 0, dels = 0;
624 git_patch_line_stats( &context, &adds, &dels, patch.get() );
625 size_t updated = std::min( adds, dels );
626 adds -= updated;
627 dels -= updated;
628 msg << wxS( "\n" ) << wxString::FromUTF8( delta->new_file.path )
629 << wxS( " " ) << adds << wxS( "/" ) << dels << wxS( "/" ) << updated;
630 }
631
632 git_oid commit_id;
633 git_commit* parentPtr = parent.get();
634 const git_commit* constParentPtr = parentPtr;
635 if( git_commit_create( &commit_id, repo, "HEAD", sig.get(), sig.get(), nullptr, msg.mb_str().data(), tree.get(),
636 parents, parentPtr ? &constParentPtr : nullptr )
637 != 0 )
638 {
640 }
641
642 git_index_write( index );
644}
645
646
647bool LOCAL_HISTORY::CommitSnapshot( const std::vector<wxString>& aFiles, const wxString& aTitle )
648{
649 if( aFiles.empty() || !Pgm().GetCommonSettings()->m_Backup.enabled )
650 return true;
651
652 wxString proj = wxFileName( aFiles[0] ).GetPath();
653 wxString hist = historyPath( proj );
654
655 // Acquire locks using hybrid locking strategy
656 HISTORY_LOCK_MANAGER lock( proj );
657
658 if( !lock.IsLocked() )
659 {
660 wxLogTrace( traceAutoSave, wxS("[history] CommitSnapshot failed to acquire lock: %s"),
661 lock.GetLockError() );
662 return false;
663 }
664
665 git_repository* repo = lock.GetRepository();
666 git_index* index = lock.GetIndex();
667
668 return commitSnapshotWithLock( repo, index, hist, proj, aFiles, aTitle ) == SNAPSHOT_COMMIT_RESULT::Committed;
669}
670
671
672// Helper to collect all project files (excluding .history, backups, and transient files)
673static void collectProjectFiles( const wxString& aProjectPath, std::vector<wxString>& aFiles )
674{
675 wxDir dir( aProjectPath );
676
677 if( !dir.IsOpened() )
678 return;
679
680 // Collect recursively.
681 std::function<void(const wxString&)> collect = [&]( const wxString& path )
682 {
683 wxString name;
684 wxDir d( path );
685
686 if( !d.IsOpened() )
687 return;
688
689 bool cont = d.GetFirst( &name );
690
691 while( cont )
692 {
693 if( name == wxS( ".history" ) || name.EndsWith( wxS( "-backups" ) ) )
694 {
695 cont = d.GetNext( &name );
696 continue; // Skip history repo itself.
697 }
698
699 wxFileName fn( path, name );
700 wxString fullPath = fn.GetFullPath();
701
702 if( wxFileName::DirExists( fullPath ) )
703 {
704 collect( fullPath );
705 }
706 else if( fn.FileExists() )
707 {
708 // Reuse NoteFileChange filters implicitly by skipping the transient names.
709 if( fn.GetFullName() != wxS( "fp-info-cache" ) )
710 aFiles.push_back( fn.GetFullPath() );
711 }
712
713 cont = d.GetNext( &name );
714 }
715 };
716
717 collect( aProjectPath );
718}
719
720
721bool LOCAL_HISTORY::CommitFullProjectSnapshot( const wxString& aProjectPath, const wxString& aTitle )
722{
723 std::vector<wxString> files;
724 collectProjectFiles( aProjectPath, files );
725
726 if( files.empty() )
727 return false;
728
729 return CommitSnapshot( files, aTitle );
730}
731
732bool LOCAL_HISTORY::HistoryExists( const wxString& aProjectPath )
733{
734 return wxDirExists( historyPath( aProjectPath ) );
735}
736
737bool LOCAL_HISTORY::TagSave( const wxString& aProjectPath, const wxString& aFileType )
738{
739 HISTORY_LOCK_MANAGER lock( aProjectPath );
740
741 if( !lock.IsLocked() )
742 {
743 wxLogTrace( traceAutoSave, wxS( "[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
744 return false;
745 }
746
747 git_repository* repo = lock.GetRepository();
748
749 if( !repo )
750 return false;
751
752 git_oid head;
753 if( git_reference_name_to_id( &head, repo, "HEAD" ) != 0 )
754 return false;
755
756 wxString tagName;
757 int i = 1;
758 git_reference* ref = nullptr;
759 do
760 {
761 tagName.Printf( wxS( "Save_%s_%d" ), aFileType, i++ );
762 } while( git_reference_lookup( &ref, repo, ( wxS( "refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
763
764 git_oid tag_oid;
765 git_object* head_obj = nullptr;
766 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
767 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
768 git_object_free( head_obj );
769
770 wxString lastName;
771 lastName.Printf( wxS( "Last_Save_%s" ), aFileType );
772 if( git_reference_lookup( &ref, repo, ( wxS( "refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
773 {
774 git_reference_delete( ref );
775 git_reference_free( ref );
776 }
777
778 git_oid last_tag_oid;
779 git_object* head_obj2 = nullptr;
780 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
781 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
782 git_object_free( head_obj2 );
783
784 return true;
785}
786
787bool LOCAL_HISTORY::HeadNewerThanLastSave( const wxString& aProjectPath )
788{
789 wxString hist = historyPath( aProjectPath );
790 git_repository* repo = nullptr;
791
792 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
793 return false;
794
795 git_oid head_oid;
796 if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
797 {
798 git_repository_free( repo );
799 return false;
800 }
801
802 git_commit* head_commit = nullptr;
803 git_commit_lookup( &head_commit, repo, &head_oid );
804 git_time_t head_time = git_commit_time( head_commit );
805
806 git_strarray tags;
807 git_tag_list_match( &tags, "Last_Save_*", repo );
808 git_time_t save_time = 0;
809
810 for( size_t i = 0; i < tags.count; ++i )
811 {
812 git_reference* ref = nullptr;
813 if( git_reference_lookup( &ref, repo,
814 ( wxS( "refs/tags/" ) +
815 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
816 {
817 const git_oid* oid = git_reference_target( ref );
818 git_commit* c = nullptr;
819 if( git_commit_lookup( &c, repo, oid ) == 0 )
820 {
821 git_time_t t = git_commit_time( c );
822 if( t > save_time )
823 save_time = t;
824 git_commit_free( c );
825 }
826 git_reference_free( ref );
827 }
828 }
829
830 git_strarray_free( &tags );
831 git_commit_free( head_commit );
832 git_repository_free( repo );
833
834 // If there are no Last_Save tags but there IS a HEAD commit, we have autosaved
835 // data that was never explicitly saved - offer to restore
836 if( save_time == 0 )
837 return true;
838
839 return head_time > save_time;
840}
841
842bool LOCAL_HISTORY::CommitDuplicateOfLastSave( const wxString& aProjectPath, const wxString& aFileType,
843 const wxString& aMessage )
844{
845 HISTORY_LOCK_MANAGER lock( aProjectPath );
846
847 if( !lock.IsLocked() )
848 {
849 wxLogTrace( traceAutoSave, wxS( "[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
850 return false;
851 }
852
853 git_repository* repo = lock.GetRepository();
854
855 if( !repo )
856 return false;
857
858 wxString lastName; lastName.Printf( wxS("Last_Save_%s"), aFileType );
859 git_reference* lastRef = nullptr;
860 if( git_reference_lookup( &lastRef, repo, ( wxS("refs/tags/") + lastName ).mb_str().data() ) != 0 )
861 return false; // no tag to duplicate
862 std::unique_ptr<git_reference, decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
863
864 const git_oid* lastOid = git_reference_target( lastRef );
865 git_commit* lastCommit = nullptr;
866 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
867 return false;
868 std::unique_ptr<git_commit, decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
869
870 git_tree* lastTree = nullptr;
871 git_commit_tree( &lastTree, lastCommit );
872 std::unique_ptr<git_tree, decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
873
874 // Parent will be current HEAD (to keep linear history)
875 git_oid headOid;
876 git_commit* headCommit = nullptr;
877 int parents = 0;
878 const git_commit* parentArray[1];
879 if( git_reference_name_to_id( &headOid, repo, "HEAD" ) == 0 &&
880 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
881 {
882 parentArray[0] = headCommit;
883 parents = 1;
884 }
885
886 git_signature* sigRaw = nullptr;
887 git_signature_now( &sigRaw, "KiCad", "[email protected]" );
888 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
889
890 wxString msg = aMessage.IsEmpty() ? wxS("Discard unsaved ") + aFileType : aMessage;
891 git_oid newCommitOid;
892 int rc = git_commit_create( &newCommitOid, repo, "HEAD", sig.get(), sig.get(), nullptr,
893 msg.mb_str().data(), lastTree, parents, parents ? parentArray : nullptr );
894 if( headCommit ) git_commit_free( headCommit );
895 if( rc != 0 )
896 return false;
897
898 // Move Last_Save tag to new commit
899 git_reference* existing = nullptr;
900 if( git_reference_lookup( &existing, repo, ( wxS("refs/tags/") + lastName ).mb_str().data() ) == 0 )
901 {
902 git_reference_delete( existing );
903 git_reference_free( existing );
904 }
905 git_object* newCommitObj = nullptr;
906 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
907 {
908 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
909 git_object_free( newCommitObj );
910 }
911 return true;
912}
913
914static size_t dirSizeRecursive( const wxString& path )
915{
916 size_t total = 0;
917 wxDir dir( path );
918 if( !dir.IsOpened() )
919 return 0;
920 wxString name;
921 bool cont = dir.GetFirst( &name );
922 while( cont )
923 {
924 wxFileName fn( path, name );
925 wxString fullPath = fn.GetFullPath();
926
927 if( wxFileName::DirExists( fullPath ) )
928 total += dirSizeRecursive( fullPath );
929 else if( fn.FileExists() )
930 total += (size_t) fn.GetSize().GetValue();
931 cont = dir.GetNext( &name );
932 }
933 return total;
934}
935
936bool LOCAL_HISTORY::EnforceSizeLimit( const wxString& aProjectPath, size_t aMaxBytes )
937{
938 if( aMaxBytes == 0 )
939 return false;
940
941 wxString hist = historyPath( aProjectPath );
942
943 if( !wxDirExists( hist ) )
944 return false;
945
946 size_t current = dirSizeRecursive( hist );
947
948 if( current <= aMaxBytes )
949 return true; // within limit
950
951 HISTORY_LOCK_MANAGER lock( aProjectPath );
952
953 if( !lock.IsLocked() )
954 {
955 wxLogTrace( traceAutoSave, wxS( "[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
956 return false;
957 }
958
959 git_repository* repo = lock.GetRepository();
960
961 if( !repo )
962 return false;
963
964 // Collect commits newest-first using revwalk
965 git_revwalk* walk = nullptr;
966 git_revwalk_new( &walk, repo );
967 git_revwalk_sorting( walk, GIT_SORT_TIME );
968 git_revwalk_push_head( walk );
969 std::vector<git_oid> commits;
970 git_oid oid;
971
972 while( git_revwalk_next( &oid, walk ) == 0 )
973 commits.push_back( oid );
974
975 git_revwalk_free( walk );
976
977 if( commits.empty() )
978 return true;
979
980 // Determine set of newest commits to keep based on blob sizes.
981 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )> seenBlobs(
982 []( const git_oid& a, const git_oid& b )
983 {
984 return memcmp( &a, &b, sizeof( git_oid ) ) < 0;
985 } );
986
987 size_t keptBytes = 0;
988 std::vector<git_oid> keep;
989
990 git_odb* odb = nullptr;
991 git_repository_odb( &odb, repo );
992
993 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
994 {
995 size_t added = 0;
996 size_t cnt = git_tree_entrycount( tree );
997
998 for( size_t i = 0; i < cnt; ++i )
999 {
1000 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1001
1002 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1003 {
1004 const git_oid* bid = git_tree_entry_id( entry );
1005
1006 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1007 {
1008 size_t len = 0;
1009 git_object_t type = GIT_OBJECT_ANY;
1010
1011 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1012 added += len;
1013
1014 seenBlobs.insert( *bid );
1015 }
1016 }
1017 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1018 {
1019 git_tree* sub = nullptr;
1020
1021 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1022 {
1023 added += accountTree( sub );
1024 git_tree_free( sub );
1025 }
1026 }
1027 }
1028
1029 return added;
1030 };
1031
1032 for( const git_oid& cOid : commits )
1033 {
1034 git_commit* c = nullptr;
1035
1036 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1037 continue;
1038
1039 git_tree* tree = nullptr;
1040 git_commit_tree( &tree, c );
1041 size_t add = accountTree( tree );
1042 git_tree_free( tree );
1043 git_commit_free( c );
1044
1045 if( keep.empty() || keptBytes + add <= aMaxBytes )
1046 {
1047 keep.push_back( cOid );
1048 keptBytes += add;
1049 }
1050 else
1051 break; // stop once limit exceeded
1052 }
1053
1054 git_odb_free( odb );
1055
1056 if( keep.empty() )
1057 keep.push_back( commits.front() ); // ensure at least head
1058
1059 // Collect tags we want to preserve (Save_*/Last_Save_*). We'll recreate them if their
1060 // target commit is retained. Also ensure tagged commits are ALWAYS kept.
1061 std::vector<std::pair<wxString, git_oid>> tagTargets;
1062 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )> taggedCommits(
1063 []( const git_oid& a, const git_oid& b )
1064 {
1065 return memcmp( &a, &b, sizeof( git_oid ) ) < 0;
1066 } );
1067 git_strarray tagList;
1068
1069 if( git_tag_list( &tagList, repo ) == 0 )
1070 {
1071 for( size_t i = 0; i < tagList.count; ++i )
1072 {
1073 wxString name = wxString::FromUTF8( tagList.strings[i] );
1074 if( name.StartsWith( wxS("Save_") ) || name.StartsWith( wxS("Last_Save_") ) )
1075 {
1076 git_reference* tref = nullptr;
1077
1078 if( git_reference_lookup( &tref, repo, ( wxS( "refs/tags/" ) + name ).mb_str().data() ) == 0 )
1079 {
1080 const git_oid* toid = git_reference_target( tref );
1081
1082 if( toid )
1083 {
1084 tagTargets.emplace_back( name, *toid );
1085 taggedCommits.insert( *toid );
1086
1087 // Ensure this tagged commit is in the keep list
1088 bool found = false;
1089 for( const auto& k : keep )
1090 {
1091 if( memcmp( &k, toid, sizeof( git_oid ) ) == 0 )
1092 {
1093 found = true;
1094 break;
1095 }
1096 }
1097
1098 if( !found )
1099 {
1100 // Add tagged commit to keep list (even if it exceeds size limit)
1101 keep.push_back( *toid );
1102 wxLogTrace( traceAutoSave, wxS( "[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1103 name );
1104 }
1105 }
1106
1107 git_reference_free( tref );
1108 }
1109 }
1110 }
1111 git_strarray_free( &tagList );
1112 }
1113
1114 // Rebuild trimmed repo in temp dir
1115 wxFileName trimFn( hist + wxS("_trim"), wxEmptyString );
1116 wxString trimPath = trimFn.GetPath();
1117
1118 if( wxDirExists( trimPath ) )
1119 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1120
1121 wxMkdir( trimPath );
1122 git_repository* newRepo = nullptr;
1123
1124 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1125 return false;
1126
1127 // We will materialize each kept commit chronologically (oldest first) to preserve order.
1128 std::reverse( keep.begin(), keep.end() );
1129 git_commit* parent = nullptr;
1130 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1131 std::vector<MAP_ENTRY> commitMap;
1132
1133 for( const git_oid& co : keep )
1134 {
1135 git_commit* orig = nullptr;
1136
1137 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1138 continue;
1139
1140 git_tree* tree = nullptr;
1141 git_commit_tree( &tree, orig );
1142
1143 // Checkout tree into filesystem (simple extractor)
1144 // Remove all files first (except .git).
1145 wxArrayString toDelete;
1146 wxDir d( trimPath );
1147 wxString nm;
1148 bool cont = d.GetFirst( &nm );
1149 while( cont )
1150 {
1151 if( nm != wxS(".git") )
1152 toDelete.Add( nm );
1153 cont = d.GetNext( &nm );
1154 }
1155
1156 for( auto& del : toDelete )
1157 {
1158 wxFileName f( trimPath, del );
1159 if( f.DirExists() )
1160 wxFileName::Rmdir( f.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1161 else if( f.FileExists() )
1162 wxRemoveFile( f.GetFullPath() );
1163 }
1164
1165 // Recursively write tree entries
1166 std::function<void(git_tree*, const wxString&)> writeTree = [&]( git_tree* t, const wxString& base )
1167 {
1168 size_t ecnt = git_tree_entrycount( t );
1169 for( size_t i = 0; i < ecnt; ++i )
1170 {
1171 const git_tree_entry* e = git_tree_entry_byindex( t, i );
1172 wxString name = wxString::FromUTF8( git_tree_entry_name( e ) );
1173
1174 if( git_tree_entry_type( e ) == GIT_OBJECT_TREE )
1175 {
1176 wxFileName dir( base, name );
1177 wxMkdir( dir.GetFullPath() );
1178 git_tree* sub = nullptr;
1179
1180 if( git_tree_lookup( &sub, repo, git_tree_entry_id( e ) ) == 0 )
1181 {
1182 writeTree( sub, dir.GetFullPath() );
1183 git_tree_free( sub );
1184 }
1185 }
1186 else if( git_tree_entry_type( e ) == GIT_OBJECT_BLOB )
1187 {
1188 git_blob* blob = nullptr;
1189
1190 if( git_blob_lookup( &blob, repo, git_tree_entry_id( e ) ) == 0 )
1191 {
1192 wxFileName file( base, name );
1193 wxFFile f( file.GetFullPath(), wxT("wb") );
1194
1195 if( f.IsOpened() )
1196 {
1197 f.Write( (const char*) git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1198 f.Close();
1199 }
1200 git_blob_free( blob );
1201 }
1202 }
1203 }
1204 };
1205
1206 writeTree( tree, trimPath );
1207
1208 git_index* newIndex = nullptr;
1209 git_repository_index( &newIndex, newRepo );
1210 git_index_add_all( newIndex, nullptr, 0, nullptr, nullptr );
1211 git_index_write( newIndex );
1212 git_oid newTreeOid;
1213 git_index_write_tree( &newTreeOid, newIndex );
1214 git_tree* newTree = nullptr;
1215 git_tree_lookup( &newTree, newRepo, &newTreeOid );
1216
1217 // Recreate original author/committer signatures preserving timestamp.
1218 const git_signature* origAuthor = git_commit_author( orig );
1219 const git_signature* origCommitter = git_commit_committer( orig );
1220 git_signature* sigAuthor = nullptr;
1221 git_signature* sigCommitter = nullptr;
1222
1223 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1224 origAuthor->when.time, origAuthor->when.offset );
1225 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1226 origCommitter->when.time, origCommitter->when.offset );
1227
1228 const git_commit* parents[1];
1229 int parentCount = 0;
1230
1231 if( parent )
1232 {
1233 parents[0] = parent;
1234 parentCount = 1;
1235 }
1236
1237 git_oid newCommitOid;
1238 git_commit_create( &newCommitOid, newRepo, "HEAD", sigAuthor, sigCommitter, nullptr, git_commit_message( orig ),
1239 newTree, parentCount, parentCount ? parents : nullptr );
1240 if( parent )
1241 git_commit_free( parent );
1242
1243 git_commit_lookup( &parent, newRepo, &newCommitOid );
1244
1245 commitMap.emplace_back( co, newCommitOid );
1246
1247 git_signature_free( sigAuthor );
1248 git_signature_free( sigCommitter );
1249 git_tree_free( newTree );
1250 git_index_free( newIndex );
1251 git_tree_free( tree );
1252 git_commit_free( orig );
1253 }
1254
1255 if( parent )
1256 git_commit_free( parent );
1257
1258 // Recreate preserved tags pointing to new commit OIDs where possible.
1259 for( const auto& tt : tagTargets )
1260 {
1261 // Find mapping
1262 const git_oid* newOid = nullptr;
1263
1264 for( const auto& m : commitMap )
1265 {
1266 if( memcmp( &m.orig, &tt.second, sizeof( git_oid ) ) == 0 )
1267 {
1268 newOid = &m.neu;
1269 break;
1270 }
1271 }
1272
1273 if( !newOid )
1274 continue; // commit trimmed away
1275
1276 git_object* obj = nullptr;
1277
1278 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1279 {
1280 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1281 git_object_free( obj );
1282 }
1283 }
1284
1285 // Close both repositories before swapping directories to avoid file locking issues.
1286 // Note: The lock manager will automatically free the original repo when it goes out of scope,
1287 // but we need to manually free the new trimmed repo we created.
1288 git_repository_free( newRepo );
1289
1290 // Replace old history dir with trimmed one
1291 wxString backupOld = hist + wxS("_old");
1292 wxRenameFile( hist, backupOld );
1293 wxRenameFile( trimPath, hist );
1294 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1295 return true;
1296}
1297
1298wxString LOCAL_HISTORY::GetHeadHash( const wxString& aProjectPath )
1299{
1300 wxString hist = historyPath( aProjectPath );
1301 git_repository* repo = nullptr;
1302
1303 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1304 return wxEmptyString;
1305
1306 git_oid head_oid;
1307 if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
1308 {
1309 git_repository_free( repo );
1310 return wxEmptyString;
1311 }
1312
1313 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1314 git_repository_free( repo );
1315 return hash;
1316}
1317
1318
1319// Helper functions for RestoreCommit
1320namespace
1321{
1322
1326bool checkForLockedFiles( const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
1327{
1328 std::function<void( const wxString& )> findLocks = [&]( const wxString& dirPath )
1329 {
1330 wxDir dir( dirPath );
1331 if( !dir.IsOpened() )
1332 return;
1333
1334 wxString filename;
1335 bool cont = dir.GetFirst( &filename );
1336
1337 while( cont )
1338 {
1339 wxFileName fullPath( dirPath, filename );
1340
1341 // Skip special directories
1342 if( filename == wxS(".history") || filename == wxS(".git") )
1343 {
1344 cont = dir.GetNext( &filename );
1345 continue;
1346 }
1347
1348 if( fullPath.DirExists() )
1349 {
1350 findLocks( fullPath.GetFullPath() );
1351 }
1352 else if( fullPath.FileExists()
1353 && filename.StartsWith( FILEEXT::LockFilePrefix )
1354 && filename.EndsWith( wxString( wxS( "." ) ) + FILEEXT::LockFileExtension ) )
1355 {
1356 // Reconstruct the original filename from the lock file name
1357 // Lock files are: ~<original>.<ext>.lck -> need to get <original>.<ext>
1358 wxString baseName = filename.Mid( FILEEXT::LockFilePrefix.length() );
1359 baseName = baseName.BeforeLast( '.' ); // Remove .lck
1360 wxFileName originalFile( dirPath, baseName );
1361
1362 // Check if this is a valid LOCKFILE (not stale and not ours)
1363 LOCKFILE testLock( originalFile.GetFullPath() );
1364 if( testLock.Valid() && !testLock.IsLockedByMe() )
1365 {
1366 aLockedFiles.push_back( fullPath.GetFullPath() );
1367 }
1368 }
1369
1370 cont = dir.GetNext( &filename );
1371 }
1372 };
1373
1374 findLocks( aProjectPath );
1375 return aLockedFiles.empty();
1376}
1377
1378
1382bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree, const wxString& aTempPath )
1383{
1384 bool extractSuccess = true;
1385
1386 std::function<void( git_tree*, const wxString& )> extractTree =
1387 [&]( git_tree* t, const wxString& prefix )
1388 {
1389 if( !extractSuccess )
1390 return;
1391
1392 size_t cnt = git_tree_entrycount( t );
1393 for( size_t i = 0; i < cnt; ++i )
1394 {
1395 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1396 wxString name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1397 wxString fullPath = prefix.IsEmpty() ? name : prefix + wxS("/") + name;
1398
1399 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1400 {
1401 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
1402 wxEmptyString );
1403 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1404 {
1405 wxLogTrace( traceAutoSave,
1406 wxS( "[history] extractCommitToTemp: Failed to create directory '%s'" ),
1407 dirPath.GetPath() );
1408 extractSuccess = false;
1409 return;
1410 }
1411
1412 git_tree* sub = nullptr;
1413 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
1414 {
1415 extractTree( sub, fullPath );
1416 git_tree_free( sub );
1417 }
1418 }
1419 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1420 {
1421 git_blob* blob = nullptr;
1422 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
1423 {
1424 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
1425
1426 wxFileName dstDir( dst );
1427 dstDir.SetFullName( wxEmptyString );
1428 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1429
1430 wxFFile f( dst.GetFullPath(), wxT( "wb" ) );
1431 if( f.IsOpened() )
1432 {
1433 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1434 f.Close();
1435 }
1436 else
1437 {
1438 wxLogTrace( traceAutoSave,
1439 wxS( "[history] extractCommitToTemp: Failed to write '%s'" ),
1440 dst.GetFullPath() );
1441 extractSuccess = false;
1442 git_blob_free( blob );
1443 return;
1444 }
1445
1446 git_blob_free( blob );
1447 }
1448 }
1449 }
1450 };
1451
1452 extractTree( aTree, wxEmptyString );
1453 return extractSuccess;
1454}
1455
1456
1460void collectFilesInDirectory( const wxString& aRootPath, const wxString& aSearchPath,
1461 std::set<wxString>& aFiles )
1462{
1463 wxDir dir( aSearchPath );
1464 if( !dir.IsOpened() )
1465 return;
1466
1467 wxString filename;
1468 bool cont = dir.GetFirst( &filename );
1469
1470 while( cont )
1471 {
1472 wxFileName fullPath( aSearchPath, filename );
1473 wxString relativePath = fullPath.GetFullPath().Mid( aRootPath.Length() + 1 );
1474
1475 if( fullPath.IsDir() && fullPath.DirExists() )
1476 {
1477 collectFilesInDirectory( aRootPath, fullPath.GetFullPath(), aFiles );
1478 }
1479 else if( fullPath.FileExists() )
1480 {
1481 aFiles.insert( relativePath );
1482 }
1483
1484 cont = dir.GetNext( &filename );
1485 }
1486}
1487
1488
1492bool shouldExcludeFromBackup( const wxString& aFilename )
1493{
1494 // Files explicitly excluded from backup should not be deleted during restore
1495 return aFilename == wxS( "fp-info-cache" );
1496}
1497
1498
1502void findFilesToDelete( const wxString& aProjectPath, const std::set<wxString>& aRestoredFiles,
1503 std::vector<wxString>& aFilesToDelete )
1504{
1505 std::function<void( const wxString&, const wxString& )> scanDirectory =
1506 [&]( const wxString& dirPath, const wxString& relativeBase )
1507 {
1508 wxDir dir( dirPath );
1509 if( !dir.IsOpened() )
1510 return;
1511
1512 wxString filename;
1513 bool cont = dir.GetFirst( &filename );
1514
1515 while( cont )
1516 {
1517 // Skip special directories
1518 if( filename == wxS(".history") || filename == wxS(".git") ||
1519 filename == wxS("_restore_backup") || filename == wxS("_restore_temp") )
1520 {
1521 cont = dir.GetNext( &filename );
1522 continue;
1523 }
1524
1525 wxFileName fullPath( dirPath, filename );
1526 wxString relativePath = relativeBase.IsEmpty() ? filename :
1527 relativeBase + wxS("/") + filename;
1528
1529 if( fullPath.IsDir() && fullPath.DirExists() )
1530 {
1531 scanDirectory( fullPath.GetFullPath(), relativePath );
1532 }
1533 else if( fullPath.FileExists() )
1534 {
1535 // Check if this file exists in the restored commit
1536 if( aRestoredFiles.find( relativePath ) == aRestoredFiles.end() )
1537 {
1538 // Don't propose deletion of files that were never in backup scope
1539 if( !shouldExcludeFromBackup( filename ) )
1540 aFilesToDelete.push_back( relativePath );
1541 }
1542 }
1543
1544 cont = dir.GetNext( &filename );
1545 }
1546 };
1547
1548 scanDirectory( aProjectPath, wxEmptyString );
1549}
1550
1551
1556bool confirmFileDeletion( wxWindow* aParent, const std::vector<wxString>& aFilesToDelete,
1557 bool& aKeepAllFiles )
1558{
1559 if( aFilesToDelete.empty() || !aParent )
1560 {
1561 aKeepAllFiles = false;
1562 return true;
1563 }
1564
1565 wxString message = _( "The following files will be deleted when restoring this commit:\n\n" );
1566
1567 // Limit display to first 20 files to avoid overwhelming dialog
1568 size_t displayCount = std::min( aFilesToDelete.size(), size_t(20) );
1569 for( size_t i = 0; i < displayCount; ++i )
1570 {
1571 message += wxS(" • ") + aFilesToDelete[i] + wxS("\n");
1572 }
1573
1574 if( aFilesToDelete.size() > displayCount )
1575 {
1576 message += wxString::Format( _( "\n... and %zu more files\n" ),
1577 aFilesToDelete.size() - displayCount );
1578 }
1579
1580 KICAD_MESSAGE_DIALOG dlg( aParent, message, _( "Delete Files during Restore" ),
1581 wxYES_NO | wxCANCEL | wxICON_QUESTION );
1582 dlg.SetYesNoCancelLabels( _( "Proceed" ), _( "Keep All Files" ), _( "Abort" ) );
1583 dlg.SetExtendedMessage(
1584 _( "Choosing 'Keep All Files' will restore the selected commit but retain any existing "
1585 "files in the project directory. Choosing 'Proceed' will delete files that are not "
1586 "present in the restored commit." ) );
1587
1588 int choice = dlg.ShowModal();
1589
1590 if( choice == wxID_CANCEL )
1591 {
1592 wxLogTrace( traceAutoSave, wxS( "[history] User cancelled restore" ) );
1593 return false;
1594 }
1595 else if( choice == wxID_NO ) // Keep All Files
1596 {
1597 wxLogTrace( traceAutoSave, wxS( "[history] User chose to keep all files" ) );
1598 aKeepAllFiles = true;
1599 }
1600 else // Proceed with deletion
1601 {
1602 wxLogTrace( traceAutoSave, wxS( "[history] User chose to proceed with deletion" ) );
1603 aKeepAllFiles = false;
1604 }
1605
1606 return true;
1607}
1608
1609
1613bool backupCurrentFiles( const wxString& aProjectPath, const wxString& aBackupPath,
1614 const wxString& aTempRestorePath, bool aKeepAllFiles,
1615 std::set<wxString>& aBackedUpFiles )
1616{
1617 wxDir currentDir( aProjectPath );
1618 if( !currentDir.IsOpened() )
1619 return false;
1620
1621 wxString filename;
1622 bool cont = currentDir.GetFirst( &filename );
1623
1624 while( cont )
1625 {
1626 if( filename != wxS( ".history" ) && filename != wxS( ".git" ) &&
1627 filename != wxS( "_restore_backup" ) && filename != wxS( "_restore_temp" ) )
1628 {
1629 // If keepAllFiles is true, only backup files that will be overwritten
1630 bool shouldBackup = !aKeepAllFiles;
1631
1632 if( aKeepAllFiles )
1633 {
1634 // Check if this file exists in the restored commit
1635 wxFileName testPath( aTempRestorePath, filename );
1636 shouldBackup = testPath.Exists();
1637 }
1638
1639 if( shouldBackup )
1640 {
1641 wxFileName source( aProjectPath, filename );
1642 wxFileName dest( aBackupPath, filename );
1643
1644 // Create backup directory if needed
1645 if( !wxDirExists( aBackupPath ) )
1646 {
1647 wxLogTrace( traceAutoSave,
1648 wxS( "[history] backupCurrentFiles: Creating backup directory %s" ),
1649 aBackupPath );
1650 wxFileName::Mkdir( aBackupPath, 0777, wxPATH_MKDIR_FULL );
1651 }
1652
1653 wxLogTrace( traceAutoSave,
1654 wxS( "[history] backupCurrentFiles: Backing up '%s' to '%s'" ),
1655 source.GetFullPath(), dest.GetFullPath() );
1656
1657 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1658 {
1659 wxLogTrace( traceAutoSave,
1660 wxS( "[history] backupCurrentFiles: Failed to backup '%s'" ),
1661 source.GetFullPath() );
1662 return false;
1663 }
1664
1665 aBackedUpFiles.insert( filename );
1666 }
1667 }
1668 cont = currentDir.GetNext( &filename );
1669 }
1670
1671 return true;
1672}
1673
1674
1678bool restoreFilesFromTemp( const wxString& aTempRestorePath, const wxString& aProjectPath,
1679 std::set<wxString>& aRestoredFiles )
1680{
1681 wxDir tempDir( aTempRestorePath );
1682 if( !tempDir.IsOpened() )
1683 return false;
1684
1685 wxString filename;
1686 bool cont = tempDir.GetFirst( &filename );
1687
1688 while( cont )
1689 {
1690 wxFileName source( aTempRestorePath, filename );
1691 wxFileName dest( aProjectPath, filename );
1692
1693 wxLogTrace( traceAutoSave,
1694 wxS( "[history] restoreFilesFromTemp: Restoring '%s' to '%s'" ),
1695 source.GetFullPath(), dest.GetFullPath() );
1696
1697 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1698 {
1699 wxLogTrace( traceAutoSave,
1700 wxS( "[history] restoreFilesFromTemp: Failed to move '%s'" ),
1701 source.GetFullPath() );
1702 return false;
1703 }
1704
1705 aRestoredFiles.insert( filename );
1706 cont = tempDir.GetNext( &filename );
1707 }
1708
1709 return true;
1710}
1711
1712
1716void rollbackRestore( const wxString& aProjectPath, const wxString& aBackupPath,
1717 const wxString& aTempRestorePath, const std::set<wxString>& aBackedUpFiles,
1718 const std::set<wxString>& aRestoredFiles )
1719{
1720 wxLogTrace( traceAutoSave, wxS( "[history] rollbackRestore: Rolling back due to failure" ) );
1721
1722 // Remove ONLY the files we successfully moved from temp directory
1723 // This preserves any files that were NOT in the backup (never tracked in history)
1724 for( const wxString& filename : aRestoredFiles )
1725 {
1726 wxFileName toRemove( aProjectPath, filename );
1727 wxLogTrace( traceAutoSave, wxS( "[history] rollbackRestore: Removing '%s'" ),
1728 toRemove.GetFullPath() );
1729
1730 if( toRemove.DirExists() )
1731 {
1732 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1733 }
1734 else if( toRemove.FileExists() )
1735 {
1736 wxRemoveFile( toRemove.GetFullPath() );
1737 }
1738 }
1739
1740 // Restore from backup - put back only what we moved
1741 if( wxDirExists( aBackupPath ) )
1742 {
1743 for( const wxString& filename : aBackedUpFiles )
1744 {
1745 wxFileName source( aBackupPath, filename );
1746 wxFileName dest( aProjectPath, filename );
1747
1748 if( source.Exists() )
1749 {
1750 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1751 wxLogTrace( traceAutoSave, wxS( "[history] rollbackRestore: Restored '%s'" ),
1752 dest.GetFullPath() );
1753 }
1754 }
1755 }
1756
1757 // Clean up temporary directories
1758 wxFileName::Rmdir( aTempRestorePath, wxPATH_RMDIR_RECURSIVE );
1759 wxFileName::Rmdir( aBackupPath, wxPATH_RMDIR_RECURSIVE );
1760}
1761
1762
1766bool recordRestoreInHistory( git_repository* aRepo, git_commit* aCommit, git_tree* aTree,
1767 const wxString& aHash )
1768{
1769 git_time_t t = git_commit_time( aCommit );
1770 wxDateTime dt( (time_t) t );
1771 git_signature* sig = nullptr;
1772 git_signature_now( &sig, "KiCad", "[email protected]" );
1773 git_commit* parent = nullptr;
1774 git_oid parent_id;
1775
1776 if( git_reference_name_to_id( &parent_id, aRepo, "HEAD" ) == 0 )
1777 git_commit_lookup( &parent, aRepo, &parent_id );
1778
1779 wxString msg;
1780 msg.Printf( wxS( "Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1781
1782 git_oid new_id;
1783 const git_commit* constParent = parent;
1784 int result = git_commit_create( &new_id, aRepo, "HEAD", sig, sig, nullptr,
1785 msg.mb_str().data(), aTree, parent ? 1 : 0,
1786 parent ? &constParent : nullptr );
1787
1788 if( parent )
1789 git_commit_free( parent );
1790 git_signature_free( sig );
1791
1792 return result == 0;
1793}
1794
1795} // namespace
1796
1797
1798bool LOCAL_HISTORY::RestoreCommit( const wxString& aProjectPath, const wxString& aHash,
1799 wxWindow* aParent )
1800{
1801 // STEP 1: Verify no files are open by checking for LOCKFILEs
1802 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Checking for open files in %s" ),
1803 aProjectPath );
1804
1805 std::vector<wxString> lockedFiles;
1806 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
1807 {
1808 wxString lockList;
1809 for( const auto& f : lockedFiles )
1810 lockList += wxS("\n - ") + f;
1811
1812 wxLogTrace( traceAutoSave,
1813 wxS( "[history] RestoreCommit: Cannot restore - files are open:%s" ),
1814 lockList );
1815
1816 // Show user-visible warning dialog
1817 if( aParent )
1818 {
1819 wxString msg = _( "Cannot restore - the following files are open by another user:" );
1820 msg += lockList;
1821 wxMessageBox( msg, _( "Restore Failed" ), wxOK | wxICON_WARNING, aParent );
1822 }
1823 return false;
1824 }
1825
1826 // STEP 2: Acquire history lock and verify target commit
1827 HISTORY_LOCK_MANAGER lock( aProjectPath );
1828
1829 if( !lock.IsLocked() )
1830 {
1831 wxLogTrace( traceAutoSave,
1832 wxS( "[history] RestoreCommit: Failed to acquire lock for %s" ),
1833 aProjectPath );
1834 return false;
1835 }
1836
1837 git_repository* repo = lock.GetRepository();
1838 if( !repo )
1839 return false;
1840
1841 // Verify the target commit exists
1842 git_oid oid;
1843 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
1844 {
1845 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Invalid hash %s" ), aHash );
1846 return false;
1847 }
1848
1849 git_commit* commit = nullptr;
1850 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
1851 {
1852 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Commit not found %s" ), aHash );
1853 return false;
1854 }
1855
1856 git_tree* tree = nullptr;
1857 git_commit_tree( &tree, commit );
1858
1859 // Create pre-restore backup snapshot using the existing lock
1860 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Creating pre-restore backup" ) );
1861
1862 std::vector<wxString> backupFiles;
1863 collectProjectFiles( aProjectPath, backupFiles );
1864
1865 if( !backupFiles.empty() )
1866 {
1867 wxString hist = historyPath( aProjectPath );
1868 SNAPSHOT_COMMIT_RESULT backupResult = commitSnapshotWithLock( repo, lock.GetIndex(), hist, aProjectPath,
1869 backupFiles, wxS( "Pre-restore backup" ) );
1870
1871 if( backupResult == SNAPSHOT_COMMIT_RESULT::Error )
1872 {
1873 wxLogTrace( traceAutoSave,
1874 wxS( "[history] RestoreCommit: Failed to create pre-restore backup" ) );
1875 git_tree_free( tree );
1876 git_commit_free( commit );
1877 return false;
1878 }
1879
1880 if( backupResult == SNAPSHOT_COMMIT_RESULT::NoChanges )
1881 {
1882 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Current state already matches HEAD; "
1883 "continuing without a new backup commit" ) );
1884 }
1885 }
1886
1887 // STEP 3: Extract commit to temporary location
1888 wxString tempRestorePath = aProjectPath + wxS("_restore_temp");
1889
1890 if( wxDirExists( tempRestorePath ) )
1891 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1892
1893 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
1894 {
1895 wxLogTrace( traceAutoSave,
1896 wxS( "[history] RestoreCommit: Failed to create temp directory %s" ),
1897 tempRestorePath );
1898 git_tree_free( tree );
1899 git_commit_free( commit );
1900 return false;
1901 }
1902
1903 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Extracting to temp location %s" ),
1904 tempRestorePath );
1905
1906 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
1907 {
1908 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Extraction failed, cleaning up" ) );
1909 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1910 git_tree_free( tree );
1911 git_commit_free( commit );
1912 return false;
1913 }
1914
1915 // STEP 4: Determine which files will be deleted and ask for confirmation
1916 std::set<wxString> restoredFiles;
1917 collectFilesInDirectory( tempRestorePath, tempRestorePath, restoredFiles );
1918
1919 std::vector<wxString> filesToDelete;
1920 findFilesToDelete( aProjectPath, restoredFiles, filesToDelete );
1921
1922 bool keepAllFiles = true;
1923 if( !confirmFileDeletion( aParent, filesToDelete, keepAllFiles ) )
1924 {
1925 // User cancelled
1926 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1927 git_tree_free( tree );
1928 git_commit_free( commit );
1929 return false;
1930 }
1931
1932 // STEP 5: Perform atomic swap - backup current, move temp to current
1933 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Performing atomic swap" ) );
1934
1935 wxString backupPath = aProjectPath + wxS("_restore_backup");
1936
1937 // Remove old backup if exists
1938 if( wxDirExists( backupPath ) )
1939 {
1940 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Removing old backup %s" ),
1941 backupPath );
1942 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1943 }
1944
1945 // Track which files we moved to backup and restored (for rollback)
1946 std::set<wxString> backedUpFiles;
1947 std::set<wxString> restoredFilesSet;
1948
1949 // Backup current files
1950 if( !backupCurrentFiles( aProjectPath, backupPath, tempRestorePath, keepAllFiles,
1951 backedUpFiles ) )
1952 {
1953 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1954 restoredFilesSet );
1955 git_tree_free( tree );
1956 git_commit_free( commit );
1957 return false;
1958 }
1959
1960 // Restore files from temp
1961 if( !restoreFilesFromTemp( tempRestorePath, aProjectPath, restoredFilesSet ) )
1962 {
1963 rollbackRestore( aProjectPath, backupPath, tempRestorePath, backedUpFiles,
1964 restoredFilesSet );
1965 git_tree_free( tree );
1966 git_commit_free( commit );
1967 return false;
1968 }
1969
1970 // SUCCESS - Clean up temp and backup directories
1971 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Restore successful, cleaning up" ) );
1972 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1973 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1974
1975 // Record the restore in history
1976 recordRestoreInHistory( repo, commit, tree, aHash );
1977
1978 git_tree_free( tree );
1979 git_commit_free( commit );
1980
1981 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Complete" ) );
1982 return true;
1983}
1984
1985void LOCAL_HISTORY::ShowRestoreDialog( const wxString& aProjectPath, wxWindow* aParent )
1986{
1987 if( !HistoryExists( aProjectPath ) )
1988 return;
1989
1990 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots = LoadSnapshots( aProjectPath );
1991
1992 if( snapshots.empty() )
1993 return;
1994
1995 DIALOG_RESTORE_LOCAL_HISTORY dlg( aParent, snapshots );
1996
1997 if( dlg.ShowModal() == wxID_OK )
1998 {
1999 wxString selectedHash = dlg.GetSelectedHash();
2000
2001 if( !selectedHash.IsEmpty() )
2002 RestoreCommit( aProjectPath, selectedHash, aParent );
2003 }
2004}
2005
2006std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> LOCAL_HISTORY::LoadSnapshots( const wxString& aProjectPath )
2007{
2008 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2009
2010 wxString hist = historyPath( aProjectPath );
2011 git_repository* repo = nullptr;
2012
2013 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2014 return snapshots;
2015
2016 git_revwalk* walk = nullptr;
2017 if( git_revwalk_new( &walk, repo ) != 0 )
2018 {
2019 git_repository_free( repo );
2020 return snapshots;
2021 }
2022
2023 git_revwalk_push_head( walk );
2024
2025 git_oid oid;
2026
2027 while( git_revwalk_next( &oid, walk ) == 0 )
2028 {
2029 git_commit* commit = nullptr;
2030
2031 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2032 continue;
2033
2035 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2036 info.date = wxDateTime( static_cast<time_t>( git_commit_time( commit ) ) );
2037 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2038
2039 wxString firstLine = info.message.BeforeFirst( '\n' );
2040
2041 long parsedCount = 0;
2042 wxString remainder;
2043 firstLine.BeforeFirst( ':', &remainder );
2044 remainder.Trim( true ).Trim( false );
2045
2046 if( remainder.EndsWith( wxS( "files changed" ) ) )
2047 {
2048 wxString countText = remainder.BeforeFirst( ' ' );
2049
2050 if( countText.ToLong( &parsedCount ) )
2051 info.filesChanged = static_cast<int>( parsedCount );
2052 }
2053
2054 info.summary = firstLine.BeforeFirst( ':' );
2055
2056 wxString rest;
2057 info.message.BeforeFirst( '\n', &rest );
2058 wxArrayString lines = wxSplit( rest, '\n', '\0' );
2059
2060 for( const wxString& line : lines )
2061 {
2062 if( !line.IsEmpty() )
2063 info.changedFiles.Add( line );
2064 }
2065
2066 snapshots.push_back( std::move( info ) );
2067 git_commit_free( commit );
2068 }
2069
2070 git_revwalk_free( walk );
2071 git_repository_free( repo );
2072 return snapshots;
2073}
int index
const char * name
int ShowModal() override
Hybrid locking mechanism for local history git repositories.
git_repository * GetRepository()
Get the git repository handle (only valid if IsLocked() returns true).
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 TagSave(const wxString &aProjectPath, const wxString &aFileType)
Tag a manual save in the local history repository.
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 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...
bool commitInBackground(const wxString &aProjectPath, const wxString &aTitle, const std::vector< HISTORY_FILE_DATA > &aFileData)
Execute file writes and git commit on a background thread.
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 EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
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
bool RunRegisteredSaversAndCommit(const wxString &aProjectPath, const wxString &aTitle)
Run all registered savers and, if any staged changes differ from HEAD, create a commit.
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 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.
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
Definition confirm.h:52
#define _(s)
static const std::string LockFileExtension
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 size_t dirSizeRecursive(const wxString &path)
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)
SNAPSHOT_COMMIT_RESULT
static void collectProjectFiles(const wxString &aProjectPath, std::vector< wxString > &aFiles)
File locking utilities.
void Prettify(std::string &aSource, FORMAT_MODE aMode)
Pretty-prints s-expression text according to KiCad format rules.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
Data produced by a registered saver on the UI thread, consumed by the background commit thread.
std::string path
wxString result
Test unit parsing edge cases and error handling.
int delta
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
wxLogTrace helper definitions.
Definition of file extensions used in Kicad.