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