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