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