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
57static std::set<wxString> s_pendingFiles;
58static std::vector<std::function<void(const wxString&, std::vector<wxString>&)>> s_savers;
59
60void LOCAL_HISTORY::NoteFileChange( const wxString& aFile )
61{
62 wxFileName fn( aFile );
63
64 if( fn.GetFullName() == wxS( "fp-info-cache" ) || fn.GetExt() == wxS( "kicad_prl" ) || !Pgm().GetCommonSettings()->m_Backup.enabled )
65 return;
66
67 s_pendingFiles.insert( fn.GetFullPath() );
68}
69
70
71void LOCAL_HISTORY::RegisterSaver( const std::function<void( const wxString&, std::vector<wxString>& )>& aSaver )
72{
73 s_savers.push_back( aSaver );
74}
75
76bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath, const wxString& aTitle )
77{
78 if( !Pgm().GetCommonSettings()->m_Backup.enabled )
79 {
80 wxLogTrace( traceAutoSave, wxS("Autosave disabled, returning" ) );
81 return true;
82 }
83
84 wxLogTrace( traceAutoSave, wxS("[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu"),
85 aProjectPath, aTitle, s_savers.size() );
86
87 if( s_savers.empty() )
88 {
89 wxLogTrace( traceAutoSave, wxS("[history] no savers registered; skipping") );
90 return false;
91 }
92
93 std::vector<wxString> files;
94 size_t idx = 0;
95
96 for( const auto& saver : s_savers )
97 {
98 size_t before = files.size();
99 saver( aProjectPath, files );
100 wxLogTrace( traceAutoSave, wxS("[history] saver #%zu added %zu files (total=%zu)"),
101 idx++, files.size() - before, files.size() );
102 }
103
104 // Filter out any files not within the project directory
105 wxString projectDir = aProjectPath;
106 if( !projectDir.EndsWith( wxFileName::GetPathSeparator() ) )
107 projectDir += wxFileName::GetPathSeparator();
108
109 auto it = std::remove_if( files.begin(), files.end(),
110 [&projectDir]( const wxString& file )
111 {
112 if( !file.StartsWith( projectDir ) )
113 {
114 wxLogTrace( traceAutoSave, wxS("[history] filtered out file outside project: %s"), file );
115 return true;
116 }
117 return false;
118 } );
119 files.erase( it, files.end() );
120
121 if( files.empty() )
122 {
123 wxLogTrace( traceAutoSave, wxS("[history] saver set produced no files; skipping") );
124 return false;
125 }
126
127 // Acquire locks using hybrid locking strategy
128 HISTORY_LOCK_MANAGER lock( aProjectPath );
129
130 if( !lock.IsLocked() )
131 {
132 wxLogTrace( traceAutoSave, wxS("[history] failed to acquire lock: %s"), lock.GetLockError() );
133 return false;
134 }
135
136 git_repository* repo = lock.GetRepository();
137 git_index* index = lock.GetIndex();
138 wxString hist = historyPath( aProjectPath );
139
140 // Stage selected files (mirroring logic from CommitSnapshot but limited to given files)
141 for( const wxString& file : files )
142 {
143 wxFileName src( file );
144
145 if( !src.FileExists() )
146 {
147 wxLogTrace( traceAutoSave, wxS("[history] skip missing '%s'"), file );
148 continue;
149 }
150
151 // If saver already produced a path inside history mirror, just stage it.
152 if( src.GetFullPath().StartsWith( hist + wxFILE_SEP_PATH ) )
153 {
154 std::string relHist = src.GetFullPath().ToStdString().substr( hist.length() + 1 );
155 git_index_add_bypath( index, relHist.c_str() );
156 wxLogTrace( traceAutoSave, wxS("[history] staged pre-mirrored '%s'"), file );
157 continue;
158 }
159
160 wxString relStr;
161 wxString proj = wxFileName( aProjectPath ).GetFullPath();
162
163 if( src.GetFullPath().StartsWith( proj + wxFILE_SEP_PATH ) )
164 relStr = src.GetFullPath().Mid( proj.length() + 1 );
165 else
166 relStr = src.GetFullName();
167
168 wxFileName dst( hist + wxFILE_SEP_PATH + relStr );
169 wxFileName dstDir( dst );
170 dstDir.SetFullName( wxEmptyString );
171 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
172 wxCopyFile( src.GetFullPath(), dst.GetFullPath(), true );
173 std::string rel = dst.GetFullPath().ToStdString().substr( hist.length() + 1 );
174 git_index_add_bypath( index, rel.c_str() );
175 wxLogTrace( traceAutoSave, wxS("[history] staged '%s' as '%s'"), file, wxString::FromUTF8( rel ) );
176 }
177
178 // Compare index to HEAD; if no diff -> abort to avoid empty commit.
179 git_oid head_oid;
180 git_commit* head_commit = nullptr;
181 git_tree* head_tree = nullptr;
182
183 bool headExists = ( git_reference_name_to_id( &head_oid, repo, "HEAD" ) == 0 )
184 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
185 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
186
187 git_tree* rawIndexTree = nullptr;
188 git_oid index_tree_oid;
189
190 if( git_index_write_tree( &index_tree_oid, index ) != 0 )
191 {
192 if( head_tree ) git_tree_free( head_tree );
193 if( head_commit ) git_commit_free( head_commit );
194 wxLogTrace( traceAutoSave, wxS("[history] failed to write index tree" ) );
195 return false;
196 }
197
198 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
199 std::unique_ptr<git_tree, decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
200
201 bool hasChanges = true;
202
203 if( headExists )
204 {
205 git_diff* diff = nullptr;
206
207 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(), nullptr ) == 0 )
208 {
209 hasChanges = git_diff_num_deltas( diff ) > 0;
210 wxLogTrace( traceAutoSave, wxS("[history] diff deltas=%u"), (unsigned) git_diff_num_deltas( diff ) );
211 git_diff_free( diff );
212 }
213 }
214
215 if( head_tree ) git_tree_free( head_tree );
216 if( head_commit ) git_commit_free( head_commit );
217
218 if( !hasChanges )
219 {
220 wxLogTrace( traceAutoSave, wxS("[history] no changes detected; no commit") );
221 return false; // Nothing new; skip commit.
222 }
223
224 git_signature* rawSig = nullptr;
225 git_signature_now( &rawSig, "KiCad", "[email protected]" );
226 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
227
228 git_commit* parent = nullptr;
229 git_oid parent_id;
230 int parents = 0;
231
232 if( git_reference_name_to_id( &parent_id, repo, "HEAD" ) == 0 )
233 {
234 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
235 parents = 1;
236 }
237
238 wxString msg = aTitle.IsEmpty() ? wxString( "Autosave" ) : aTitle;
239 git_oid commit_id;
240 const git_commit* constParent = parent;
241
242 int rc = git_commit_create( &commit_id, repo, "HEAD", sig.get(), sig.get(), nullptr,
243 msg.mb_str().data(), indexTree.get(), parents,
244 parents ? &constParent : nullptr );
245
246 if( rc == 0 )
247 wxLogTrace( traceAutoSave, wxS("[history] commit created %s (%s files=%zu)"),
248 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, files.size() );
249 else
250 wxLogTrace( traceAutoSave, wxS("[history] commit failed rc=%d"), rc );
251
252 if( parent ) git_commit_free( parent );
253
254 git_index_write( index );
255 return rc == 0;
256}
257
258
260{
261 std::vector<wxString> files( s_pendingFiles.begin(), s_pendingFiles.end() );
262 s_pendingFiles.clear();
263 return CommitSnapshot( files, wxS( "Autosave" ) );
264}
265
266
267bool LOCAL_HISTORY::Init( const wxString& aProjectPath )
268{
269 if( !Pgm().GetCommonSettings()->m_Backup.enabled )
270 return true;
271
272 wxString hist = historyPath( aProjectPath );
273
274 if( !wxDirExists( hist ) )
275 wxMkdir( hist );
276
277 git_repository* rawRepo = nullptr;
278
279 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
280 {
281 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
282 return false;
283
284 wxFileName ignoreFile( hist, wxS( ".gitignore" ) );
285 if( !ignoreFile.FileExists() )
286 {
287 wxFFile f( ignoreFile.GetFullPath(), wxT( "w" ) );
288 if( f.IsOpened() )
289 {
290 f.Write( wxS( "fp-info-cache\n*.kicad_prl\n*-backups\n" ) );
291 f.Close();
292 }
293 }
294 }
295
296 git_repository_free( rawRepo );
297 return true;
298}
299
300
301bool LOCAL_HISTORY::CommitSnapshot( const std::vector<wxString>& aFiles, const wxString& aTitle )
302{
303 if( aFiles.empty() || !Pgm().GetCommonSettings()->m_Backup.enabled )
304 return true;
305
306 wxString proj = wxFileName( aFiles[0] ).GetPath();
307 wxString hist = historyPath( proj );
308
309 // Acquire locks using hybrid locking strategy
310 HISTORY_LOCK_MANAGER lock( proj );
311
312 if( !lock.IsLocked() )
313 {
314 wxLogTrace( traceAutoSave, wxS("[history] CommitSnapshot failed to acquire lock: %s"),
315 lock.GetLockError() );
316 return false;
317 }
318
319 git_repository* repo = lock.GetRepository();
320 git_index* index = lock.GetIndex();
321
322 for( const wxString& file : aFiles )
323 {
324 wxFileName src( file );
325 wxString relStr;
326
327 if( src.GetFullPath().StartsWith( proj + wxFILE_SEP_PATH ) )
328 relStr = src.GetFullPath().Mid( proj.length() + 1 );
329 else
330 relStr = src.GetFullName(); // Fallback (should not normally happen)
331
332 wxFileName dst( hist + wxFILE_SEP_PATH + relStr );
333
334 // Ensure directory tree exists.
335 wxFileName dstDir( dst );
336 dstDir.SetFullName( wxEmptyString );
337 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
338
339 // Copy file into history mirror.
340 wxCopyFile( src.GetFullPath(), dst.GetFullPath(), true );
341
342 // Path inside repo (strip hist + '/').
343 std::string rel = dst.GetFullPath().ToStdString().substr( hist.length() + 1 );
344 git_index_add_bypath( index, rel.c_str() );
345 }
346
347 git_oid tree_id;
348 if( git_index_write_tree( &tree_id, index ) != 0 )
349 return false;
350
351 git_tree* rawTree = nullptr;
352 git_tree_lookup( &rawTree, repo, &tree_id );
353 std::unique_ptr<git_tree, decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
354
355 git_signature* rawSig = nullptr;
356 git_signature_now( &rawSig, "KiCad", "[email protected]" );
357 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( rawSig,
358 &git_signature_free );
359
360 git_commit* rawParent = nullptr;
361 git_oid parent_id;
362 int parents = 0;
363
364 if( git_reference_name_to_id( &parent_id, repo, "HEAD" ) == 0 )
365 {
366 git_commit_lookup( &rawParent, repo, &parent_id );
367 parents = 1;
368 }
369
370 std::unique_ptr<git_commit, decltype( &git_commit_free )> parent( rawParent,
371 &git_commit_free );
372
373 git_tree* rawParentTree = nullptr;
374
375 if( parent )
376 git_commit_tree( &rawParentTree, parent.get() );
377
378 std::unique_ptr<git_tree, decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
379
380 git_diff* rawDiff = nullptr;
381 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(), index, nullptr );
382 std::unique_ptr<git_diff, decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
383
384 wxString msg;
385
386 if( !aTitle.IsEmpty() )
387 msg << aTitle << wxS( ": " );
388
389 msg << aFiles.size() << wxS( " files changed" );
390
391 for( size_t i = 0; i < git_diff_num_deltas( diff.get() ); ++i )
392 {
393 const git_diff_delta* delta = git_diff_get_delta( diff.get(), i );
394 git_patch* rawPatch = nullptr;
395 git_patch_from_diff( &rawPatch, diff.get(), i );
396 std::unique_ptr<git_patch, decltype( &git_patch_free )> patch( rawPatch,
397 &git_patch_free );
398 size_t context = 0, adds = 0, dels = 0;
399 git_patch_line_stats( &context, &adds, &dels, patch.get() );
400 size_t updated = std::min( adds, dels );
401 adds -= updated;
402 dels -= updated;
403 msg << wxS( "\n" ) << wxString::FromUTF8( delta->new_file.path )
404 << wxS( " " ) << adds << wxS( "/" ) << dels << wxS( "/" ) << updated;
405 }
406
407 git_oid commit_id;
408 git_commit* parentPtr = parent.get();
409 const git_commit* constParentPtr = parentPtr;
410 git_commit_create( &commit_id, repo, "HEAD", sig.get(), sig.get(), nullptr,
411 msg.mb_str().data(), tree.get(), parents,
412 parentPtr ? &constParentPtr : nullptr );
413 git_index_write( index );
414 return true;
415}
416
417
418bool LOCAL_HISTORY::CommitFullProjectSnapshot( const wxString& aProjectPath, const wxString& aTitle )
419{
420 // Enumerate all regular files under the project path (non-recursive through .history).
421 wxArrayString files;
422 wxDir dir( aProjectPath );
423
424 if( !dir.IsOpened() )
425 return false;
426
427 // Collect recursively.
428 std::function<void(const wxString&)> collect = [&]( const wxString& path )
429 {
430 wxString name;
431 wxDir d( path );
432
433 if( !d.IsOpened() )
434 return;
435
436 bool cont = d.GetFirst( &name );
437
438 while( cont )
439 {
440 if( name == wxS( ".history" ) || name.EndsWith( wxS( "-backups" ) ) )
441 {
442 cont = d.GetNext( &name );
443 continue; // Skip history repo itself.
444 }
445
446 wxFileName fn( path, name );
447
448 if( fn.DirExists() )
449 {
450 collect( fn.GetFullPath() );
451 }
452 else if( fn.FileExists() )
453 {
454 // Reuse NoteFileChange filters implicitly by skipping the transient names.
455 if( fn.GetFullName() != wxS( "fp-info-cache" ) && fn.GetExt() != wxS( "kicad_prl" ) )
456 files.Add( fn.GetFullPath() );
457 }
458
459 cont = d.GetNext( &name );
460 }
461 };
462
463 collect( aProjectPath );
464
465 std::vector<wxString> vec;
466 vec.reserve( files.GetCount() );
467
468 for( unsigned i = 0; i < files.GetCount(); ++i )
469 vec.push_back( files[i] );
470
471 return CommitSnapshot( vec, aTitle );
472}
473
474bool LOCAL_HISTORY::HistoryExists( const wxString& aProjectPath )
475{
476 return wxDirExists( historyPath( aProjectPath ) );
477}
478
479bool LOCAL_HISTORY::TagSave( const wxString& aProjectPath, const wxString& aFileType )
480{
481 HISTORY_LOCK_MANAGER lock( aProjectPath );
482
483 if( !lock.IsLocked() )
484 {
485 wxLogTrace( traceAutoSave, wxS( "[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
486 return false;
487 }
488
489 git_repository* repo = lock.GetRepository();
490
491 if( !repo )
492 return false;
493
494 git_oid head;
495 if( git_reference_name_to_id( &head, repo, "HEAD" ) != 0 )
496 return false;
497
498 wxString tagName;
499 int i = 1;
500 git_reference* ref = nullptr;
501 do
502 {
503 tagName.Printf( wxS( "Save_%s_%d" ), aFileType, i++ );
504 } while( git_reference_lookup( &ref, repo, ( wxS( "refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
505
506 git_oid tag_oid;
507 git_object* head_obj = nullptr;
508 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
509 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
510 git_object_free( head_obj );
511
512 wxString lastName;
513 lastName.Printf( wxS( "Last_Save_%s" ), aFileType );
514 if( git_reference_lookup( &ref, repo, ( wxS( "refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
515 {
516 git_reference_delete( ref );
517 git_reference_free( ref );
518 }
519
520 git_oid last_tag_oid;
521 git_object* head_obj2 = nullptr;
522 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
523 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
524 git_object_free( head_obj2 );
525
526 return true;
527}
528
529bool LOCAL_HISTORY::HeadNewerThanLastSave( const wxString& aProjectPath )
530{
531 wxString hist = historyPath( aProjectPath );
532 git_repository* repo = nullptr;
533
534 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
535 return false;
536
537 git_oid head_oid;
538 if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
539 {
540 git_repository_free( repo );
541 return false;
542 }
543
544 git_commit* head_commit = nullptr;
545 git_commit_lookup( &head_commit, repo, &head_oid );
546 git_time_t head_time = git_commit_time( head_commit );
547
548 git_strarray tags;
549 git_tag_list_match( &tags, "Last_Save_*", repo );
550 git_time_t save_time = 0;
551
552 for( size_t i = 0; i < tags.count; ++i )
553 {
554 git_reference* ref = nullptr;
555 if( git_reference_lookup( &ref, repo,
556 ( wxS( "refs/tags/" ) +
557 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
558 {
559 const git_oid* oid = git_reference_target( ref );
560 git_commit* c = nullptr;
561 if( git_commit_lookup( &c, repo, oid ) == 0 )
562 {
563 git_time_t t = git_commit_time( c );
564 if( t > save_time )
565 save_time = t;
566 git_commit_free( c );
567 }
568 git_reference_free( ref );
569 }
570 }
571
572 git_strarray_free( &tags );
573 git_commit_free( head_commit );
574 git_repository_free( repo );
575
576 return save_time && head_time > save_time;
577}
578
579bool LOCAL_HISTORY::CommitDuplicateOfLastSave( const wxString& aProjectPath, const wxString& aFileType,
580 const wxString& aMessage )
581{
582 HISTORY_LOCK_MANAGER lock( aProjectPath );
583
584 if( !lock.IsLocked() )
585 {
586 wxLogTrace( traceAutoSave, wxS( "[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
587 return false;
588 }
589
590 git_repository* repo = lock.GetRepository();
591
592 if( !repo )
593 return false;
594
595 wxString lastName; lastName.Printf( wxS("Last_Save_%s"), aFileType );
596 git_reference* lastRef = nullptr;
597 if( git_reference_lookup( &lastRef, repo, ( wxS("refs/tags/") + lastName ).mb_str().data() ) != 0 )
598 return false; // no tag to duplicate
599 std::unique_ptr<git_reference, decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
600
601 const git_oid* lastOid = git_reference_target( lastRef );
602 git_commit* lastCommit = nullptr;
603 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
604 return false;
605 std::unique_ptr<git_commit, decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
606
607 git_tree* lastTree = nullptr;
608 git_commit_tree( &lastTree, lastCommit );
609 std::unique_ptr<git_tree, decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
610
611 // Parent will be current HEAD (to keep linear history)
612 git_oid headOid;
613 git_commit* headCommit = nullptr;
614 int parents = 0;
615 const git_commit* parentArray[1];
616 if( git_reference_name_to_id( &headOid, repo, "HEAD" ) == 0 &&
617 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
618 {
619 parentArray[0] = headCommit;
620 parents = 1;
621 }
622
623 git_signature* sigRaw = nullptr;
624 git_signature_now( &sigRaw, "KiCad", "[email protected]" );
625 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
626
627 wxString msg = aMessage.IsEmpty() ? wxS("Discard unsaved ") + aFileType : aMessage;
628 git_oid newCommitOid;
629 int rc = git_commit_create( &newCommitOid, repo, "HEAD", sig.get(), sig.get(), nullptr,
630 msg.mb_str().data(), lastTree, parents, parents ? parentArray : nullptr );
631 if( headCommit ) git_commit_free( headCommit );
632 if( rc != 0 )
633 return false;
634
635 // Move Last_Save tag to new commit
636 git_reference* existing = nullptr;
637 if( git_reference_lookup( &existing, repo, ( wxS("refs/tags/") + lastName ).mb_str().data() ) == 0 )
638 {
639 git_reference_delete( existing );
640 git_reference_free( existing );
641 }
642 git_object* newCommitObj = nullptr;
643 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
644 {
645 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
646 git_object_free( newCommitObj );
647 }
648 return true;
649}
650
651static size_t dirSizeRecursive( const wxString& path )
652{
653 size_t total = 0;
654 wxDir dir( path );
655 if( !dir.IsOpened() )
656 return 0;
657 wxString name;
658 bool cont = dir.GetFirst( &name );
659 while( cont )
660 {
661 wxFileName fn( path, name );
662 if( fn.DirExists() )
663 total += dirSizeRecursive( fn.GetFullPath() );
664 else if( fn.FileExists() )
665 total += (size_t) fn.GetSize().GetValue();
666 cont = dir.GetNext( &name );
667 }
668 return total;
669}
670
671bool LOCAL_HISTORY::EnforceSizeLimit( const wxString& aProjectPath, size_t aMaxBytes )
672{
673 if( aMaxBytes == 0 )
674 return false;
675
676 wxString hist = historyPath( aProjectPath );
677
678 if( !wxDirExists( hist ) )
679 return false;
680
681 size_t current = dirSizeRecursive( hist );
682
683 if( current <= aMaxBytes )
684 return true; // within limit
685
686 HISTORY_LOCK_MANAGER lock( aProjectPath );
687
688 if( !lock.IsLocked() )
689 {
690 wxLogTrace( traceAutoSave, wxS( "[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
691 return false;
692 }
693
694 git_repository* repo = lock.GetRepository();
695
696 if( !repo )
697 return false;
698
699 // Collect commits newest-first using revwalk
700 git_revwalk* walk = nullptr;
701 git_revwalk_new( &walk, repo );
702 git_revwalk_sorting( walk, GIT_SORT_TIME );
703 git_revwalk_push_head( walk );
704 std::vector<git_oid> commits;
705 git_oid oid;
706
707 while( git_revwalk_next( &oid, walk ) == 0 )
708 commits.push_back( oid );
709
710 git_revwalk_free( walk );
711
712 if( commits.empty() )
713 return true;
714
715 // Determine set of newest commits to keep based on blob sizes.
716 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )> seenBlobs(
717 []( const git_oid& a, const git_oid& b )
718 {
719 return memcmp( &a, &b, sizeof( git_oid ) ) < 0;
720 } );
721
722 size_t keptBytes = 0;
723 std::vector<git_oid> keep;
724
725 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
726 {
727 size_t added = 0;
728 size_t cnt = git_tree_entrycount( tree );
729 for( size_t i = 0; i < cnt; ++i )
730 {
731 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
732
733 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
734 {
735 const git_oid* bid = git_tree_entry_id( entry );
736
737 if( seenBlobs.find( *bid ) == seenBlobs.end() )
738 {
739 git_blob* blob = nullptr;
740
741 if( git_blob_lookup( &blob, repo, bid ) == 0 )
742 {
743 added += git_blob_rawsize( blob );
744 git_blob_free( blob );
745 }
746
747 seenBlobs.insert( *bid );
748 }
749 }
750 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
751 {
752 git_tree* sub = nullptr;
753
754 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
755 {
756 added += accountTree( sub );
757 git_tree_free( sub );
758 }
759 }
760 }
761 return added;
762 };
763
764 for( const git_oid& cOid : commits )
765 {
766 git_commit* c = nullptr;
767
768 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
769 continue;
770
771 git_tree* tree = nullptr;
772 git_commit_tree( &tree, c );
773 size_t add = accountTree( tree );
774 git_tree_free( tree );
775 git_commit_free( c );
776
777 if( keep.empty() || keptBytes + add <= aMaxBytes )
778 {
779 keep.push_back( cOid );
780 keptBytes += add;
781 }
782 else
783 break; // stop once limit exceeded
784 }
785
786 if( keep.empty() )
787 keep.push_back( commits.front() ); // ensure at least head
788
789 // Collect tags we want to preserve (Save_*/Last_Save_*). We'll recreate them if their
790 // target commit is retained. Also ensure tagged commits are ALWAYS kept.
791 std::vector<std::pair<wxString, git_oid>> tagTargets;
792 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )> taggedCommits(
793 []( const git_oid& a, const git_oid& b )
794 {
795 return memcmp( &a, &b, sizeof( git_oid ) ) < 0;
796 } );
797 git_strarray tagList;
798
799 if( git_tag_list( &tagList, repo ) == 0 )
800 {
801 for( size_t i = 0; i < tagList.count; ++i )
802 {
803 wxString name = wxString::FromUTF8( tagList.strings[i] );
804 if( name.StartsWith( wxS("Save_") ) || name.StartsWith( wxS("Last_Save_") ) )
805 {
806 git_reference* tref = nullptr;
807
808 if( git_reference_lookup( &tref, repo, ( wxS( "refs/tags/" ) + name ).mb_str().data() ) == 0 )
809 {
810 const git_oid* toid = git_reference_target( tref );
811
812 if( toid )
813 {
814 tagTargets.emplace_back( name, *toid );
815 taggedCommits.insert( *toid );
816
817 // Ensure this tagged commit is in the keep list
818 bool found = false;
819 for( const auto& k : keep )
820 {
821 if( memcmp( &k, toid, sizeof( git_oid ) ) == 0 )
822 {
823 found = true;
824 break;
825 }
826 }
827
828 if( !found )
829 {
830 // Add tagged commit to keep list (even if it exceeds size limit)
831 keep.push_back( *toid );
832 wxLogTrace( traceAutoSave, wxS( "[history] EnforceSizeLimit: Preserving tagged commit %s" ),
833 name );
834 }
835 }
836
837 git_reference_free( tref );
838 }
839 }
840 }
841 git_strarray_free( &tagList );
842 }
843
844 // Rebuild trimmed repo in temp dir
845 wxFileName trimFn( hist + wxS("_trim"), wxEmptyString );
846 wxString trimPath = trimFn.GetPath();
847
848 if( wxDirExists( trimPath ) )
849 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
850
851 wxMkdir( trimPath );
852 git_repository* newRepo = nullptr;
853
854 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
855 return false;
856
857 // We will materialize each kept commit chronologically (oldest first) to preserve order.
858 std::reverse( keep.begin(), keep.end() );
859 git_commit* parent = nullptr;
860 struct MAP_ENTRY { git_oid orig; git_oid neu; };
861 std::vector<MAP_ENTRY> commitMap;
862
863 for( const git_oid& co : keep )
864 {
865 git_commit* orig = nullptr;
866
867 if( git_commit_lookup( &orig, repo, &co ) != 0 )
868 continue;
869
870 git_tree* tree = nullptr;
871 git_commit_tree( &tree, orig );
872
873 // Checkout tree into filesystem (simple extractor)
874 // Remove all files first (except .git).
875 wxArrayString toDelete;
876 wxDir d( trimPath );
877 wxString nm;
878 bool cont = d.GetFirst( &nm );
879 while( cont )
880 {
881 if( nm != wxS(".git") )
882 toDelete.Add( nm );
883 cont = d.GetNext( &nm );
884 }
885
886 for( auto& del : toDelete )
887 {
888 wxFileName f( trimPath, del );
889 if( f.DirExists() )
890 wxFileName::Rmdir( f.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
891 else if( f.FileExists() )
892 wxRemoveFile( f.GetFullPath() );
893 }
894
895 // Recursively write tree entries
896 std::function<void(git_tree*, const wxString&)> writeTree = [&]( git_tree* t, const wxString& base )
897 {
898 size_t ecnt = git_tree_entrycount( t );
899 for( size_t i = 0; i < ecnt; ++i )
900 {
901 const git_tree_entry* e = git_tree_entry_byindex( t, i );
902 wxString name = wxString::FromUTF8( git_tree_entry_name( e ) );
903
904 if( git_tree_entry_type( e ) == GIT_OBJECT_TREE )
905 {
906 wxFileName dir( base, name );
907 wxMkdir( dir.GetFullPath() );
908 git_tree* sub = nullptr;
909
910 if( git_tree_lookup( &sub, repo, git_tree_entry_id( e ) ) == 0 )
911 {
912 writeTree( sub, dir.GetFullPath() );
913 git_tree_free( sub );
914 }
915 }
916 else if( git_tree_entry_type( e ) == GIT_OBJECT_BLOB )
917 {
918 git_blob* blob = nullptr;
919
920 if( git_blob_lookup( &blob, repo, git_tree_entry_id( e ) ) == 0 )
921 {
922 wxFileName file( base, name );
923 wxFFile f( file.GetFullPath(), wxT("wb") );
924
925 if( f.IsOpened() )
926 {
927 f.Write( (const char*) git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
928 f.Close();
929 }
930 git_blob_free( blob );
931 }
932 }
933 }
934 };
935
936 writeTree( tree, trimPath );
937
938 git_index* newIndex = nullptr;
939 git_repository_index( &newIndex, newRepo );
940 git_index_add_all( newIndex, nullptr, 0, nullptr, nullptr );
941 git_index_write( newIndex );
942 git_oid newTreeOid;
943 git_index_write_tree( &newTreeOid, newIndex );
944 git_tree* newTree = nullptr;
945 git_tree_lookup( &newTree, newRepo, &newTreeOid );
946
947 // Recreate original author/committer signatures preserving timestamp.
948 const git_signature* origAuthor = git_commit_author( orig );
949 const git_signature* origCommitter = git_commit_committer( orig );
950 git_signature* sigAuthor = nullptr;
951 git_signature* sigCommitter = nullptr;
952
953 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
954 origAuthor->when.time, origAuthor->when.offset );
955 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
956 origCommitter->when.time, origCommitter->when.offset );
957
958 const git_commit* parents[1];
959 int parentCount = 0;
960
961 if( parent )
962 {
963 parents[0] = parent;
964 parentCount = 1;
965 }
966
967 git_oid newCommitOid;
968 git_commit_create( &newCommitOid, newRepo, "HEAD", sigAuthor, sigCommitter, nullptr, git_commit_message( orig ),
969 newTree, parentCount, parentCount ? parents : nullptr );
970 if( parent )
971 git_commit_free( parent );
972
973 git_commit_lookup( &parent, newRepo, &newCommitOid );
974
975 commitMap.emplace_back( co, newCommitOid );
976
977 git_signature_free( sigAuthor );
978 git_signature_free( sigCommitter );
979 git_tree_free( newTree );
980 git_index_free( newIndex );
981 git_tree_free( tree );
982 git_commit_free( orig );
983 }
984
985 if( parent )
986 git_commit_free( parent );
987
988 // Recreate preserved tags pointing to new commit OIDs where possible.
989 for( const auto& tt : tagTargets )
990 {
991 // Find mapping
992 const git_oid* newOid = nullptr;
993
994 for( const auto& m : commitMap )
995 {
996 if( memcmp( &m.orig, &tt.second, sizeof( git_oid ) ) == 0 )
997 {
998 newOid = &m.neu;
999 break;
1000 }
1001 }
1002
1003 if( !newOid )
1004 continue; // commit trimmed away
1005
1006 git_object* obj = nullptr;
1007
1008 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1009 {
1010 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1011 git_object_free( obj );
1012 }
1013 }
1014
1015 // Close both repositories before swapping directories to avoid file locking issues.
1016 // Note: The lock manager will automatically free the original repo when it goes out of scope,
1017 // but we need to manually free the new trimmed repo we created.
1018 git_repository_free( newRepo );
1019
1020 // Replace old history dir with trimmed one
1021 wxString backupOld = hist + wxS("_old");
1022 wxRenameFile( hist, backupOld );
1023 wxRenameFile( trimPath, hist );
1024 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1025 return true;
1026}
1027
1028wxString LOCAL_HISTORY::GetHeadHash( const wxString& aProjectPath )
1029{
1030 wxString hist = historyPath( aProjectPath );
1031 git_repository* repo = nullptr;
1032
1033 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1034 return wxEmptyString;
1035
1036 git_oid head_oid;
1037 if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
1038 {
1039 git_repository_free( repo );
1040 return wxEmptyString;
1041 }
1042
1043 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1044 git_repository_free( repo );
1045 return hash;
1046}
1047
1048bool LOCAL_HISTORY::RestoreCommit( const wxString& aProjectPath, const wxString& aHash )
1049{
1050 // STEP 1: Verify no files are open by checking for LOCKFILEs
1051 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Checking for open files in %s" ), aProjectPath );
1052
1053 std::vector<wxString> lockedFiles;
1054 std::function<void( const wxString& )> findLocks = [&]( const wxString& dirPath )
1055 {
1056 wxDir dir( dirPath );
1057 if( !dir.IsOpened() )
1058 return;
1059
1060 wxString filename;
1061 bool cont = dir.GetFirst( &filename );
1062
1063 while( cont )
1064 {
1065 wxFileName fullPath( dirPath, filename );
1066
1067 // Skip .history directory
1068 if( filename == wxS(".history") || filename == wxS(".git") )
1069 {
1070 cont = dir.GetNext( &filename );
1071 continue;
1072 }
1073
1074 if( fullPath.DirExists() )
1075 {
1076 findLocks( fullPath.GetFullPath() );
1077 }
1078 else if( fullPath.FileExists() && filename.EndsWith( wxS(".lock") ) )
1079 {
1080 // Check if this is a valid LOCKFILE (not stale and not ours)
1081 LOCKFILE testLock( fullPath.GetFullPath().BeforeLast( '.' ) );
1082 if( testLock.Valid() && !testLock.IsLockedByMe() )
1083 {
1084 lockedFiles.push_back( fullPath.GetFullPath() );
1085 }
1086 }
1087
1088 cont = dir.GetNext( &filename );
1089 }
1090 };
1091
1092 findLocks( aProjectPath );
1093
1094 if( !lockedFiles.empty() )
1095 {
1096 wxString lockList;
1097 for( const auto& f : lockedFiles )
1098 lockList += wxS("\n - ") + f;
1099
1100 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Cannot restore - files are open:%s" ), lockList );
1101 return false;
1102 }
1103
1104 // STEP 2: Acquire history lock and create backup snapshot
1105 HISTORY_LOCK_MANAGER lock( aProjectPath );
1106
1107 if( !lock.IsLocked() )
1108 {
1109 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Failed to acquire lock for %s" ), aProjectPath );
1110 return false;
1111 }
1112
1113 git_repository* repo = lock.GetRepository();
1114 if( !repo )
1115 return false;
1116
1117 // Verify the target commit exists
1118 git_oid oid;
1119 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
1120 {
1121 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Invalid hash %s" ), aHash );
1122 return false;
1123 }
1124
1125 git_commit* commit = nullptr;
1126 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
1127 {
1128 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Commit not found %s" ), aHash );
1129 return false;
1130 }
1131
1132 git_tree* tree = nullptr;
1133 git_commit_tree( &tree, commit );
1134
1135 // Create pre-restore backup snapshot
1136 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Creating pre-restore backup" ) );
1137 wxString backupHash = GetHeadHash( aProjectPath );
1138
1139 if( !CommitFullProjectSnapshot( aProjectPath, wxS("Pre-restore backup") ) )
1140 {
1141 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Failed to create pre-restore backup" ) );
1142 git_tree_free( tree );
1143 git_commit_free( commit );
1144 return false;
1145 }
1146
1147 // STEP 3: Extract to temporary location
1148 wxString tempRestorePath = aProjectPath + wxS("_restore_temp");
1149
1150 if( wxDirExists( tempRestorePath ) )
1151 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1152
1153 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
1154 {
1155 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Failed to create temp directory %s" ), tempRestorePath );
1156 git_tree_free( tree );
1157 git_commit_free( commit );
1158 return false;
1159 }
1160
1161 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Extracting to temp location %s" ), tempRestorePath );
1162
1163 // Recursively extract git tree to temp location
1164 bool extractSuccess = true;
1165 std::function<void( git_tree*, const wxString& )> extractTree = [&]( git_tree* t, const wxString& prefix )
1166 {
1167 if( !extractSuccess )
1168 return;
1169
1170 size_t cnt = git_tree_entrycount( t );
1171 for( size_t i = 0; i < cnt; ++i )
1172 {
1173 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
1174 wxString name = wxString::FromUTF8( git_tree_entry_name( entry ) );
1175 wxString fullPath = prefix.IsEmpty() ? name : prefix + wxS("/") + name;
1176
1177 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1178 {
1179 wxFileName dirPath( tempRestorePath + wxFileName::GetPathSeparator() + fullPath, wxEmptyString );
1180 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
1181 {
1182 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Failed to create directory '%s'" ),
1183 dirPath.GetPath() );
1184 extractSuccess = false;
1185 return;
1186 }
1187
1188 git_tree* sub = nullptr;
1189 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1190 {
1191 extractTree( sub, fullPath );
1192 git_tree_free( sub );
1193 }
1194 }
1195 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1196 {
1197 git_blob* blob = nullptr;
1198 if( git_blob_lookup( &blob, repo, git_tree_entry_id( entry ) ) == 0 )
1199 {
1200 wxFileName dst( tempRestorePath + wxFileName::GetPathSeparator() + fullPath );
1201
1202 wxFileName dstDir( dst );
1203 dstDir.SetFullName( wxEmptyString );
1204 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
1205
1206 wxFFile f( dst.GetFullPath(), wxT( "wb" ) );
1207 if( f.IsOpened() )
1208 {
1209 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
1210 f.Close();
1211 }
1212 else
1213 {
1214 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Failed to write '%s'" ),
1215 dst.GetFullPath() );
1216 extractSuccess = false;
1217 git_blob_free( blob );
1218 return;
1219 }
1220
1221 git_blob_free( blob );
1222 }
1223 }
1224 }
1225 };
1226
1227 extractTree( tree, wxEmptyString );
1228
1229 if( !extractSuccess )
1230 {
1231 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Extraction failed, cleaning up" ) );
1232 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1233 git_tree_free( tree );
1234 git_commit_free( commit );
1235 return false;
1236 }
1237
1238 // STEP 4: Atomic move - backup current, move temp to current
1239 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Performing atomic swap" ) );
1240
1241 wxString backupPath = aProjectPath + wxS("_restore_backup");
1242
1243 // Remove old backup if exists
1244 if( wxDirExists( backupPath ) )
1245 {
1246 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Removing old backup %s" ), backupPath );
1247 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1248 }
1249
1250 // Track which files we moved to backup (for safe rollback)
1251 std::set<wxString> backedUpFiles;
1252
1253 // Move current project to backup (except .history)
1254 bool moveSuccess = true;
1255 wxDir currentDir( aProjectPath );
1256
1257 if( currentDir.IsOpened() )
1258 {
1259 wxString filename;
1260 bool cont = currentDir.GetFirst( &filename );
1261
1262 while( cont )
1263 {
1264 if( filename != wxS(".history") && filename != wxS(".git") )
1265 {
1266 wxFileName source( aProjectPath, filename );
1267 wxFileName dest( backupPath, filename );
1268
1269 // Create backup directory if needed
1270 if( !wxDirExists( backupPath ) )
1271 {
1272 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Creating backup directory %s" ), backupPath );
1273 wxFileName::Mkdir( backupPath, 0777, wxPATH_MKDIR_FULL );
1274 }
1275
1276 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Backing up '%s' to '%s'" ),
1277 source.GetFullPath(), dest.GetFullPath() );
1278 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1279 {
1280 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Failed to backup '%s'" ),
1281 source.GetFullPath() );
1282 moveSuccess = false;
1283 break;
1284 }
1285
1286 backedUpFiles.insert( filename );
1287 }
1288 cont = currentDir.GetNext( &filename );
1289 }
1290 }
1291
1292 // Track which files we successfully moved from temp (for rollback)
1293 std::set<wxString> restoredFiles;
1294
1295 // STEP 5: If backup succeeded, move temp files to project directory
1296 if( moveSuccess )
1297 {
1298 wxDir tempDir( tempRestorePath );
1299
1300 if( tempDir.IsOpened() )
1301 {
1302 wxString filename;
1303 bool cont = tempDir.GetFirst( &filename );
1304
1305 while( cont )
1306 {
1307 wxFileName source( tempRestorePath, filename );
1308 wxFileName dest( aProjectPath, filename );
1309
1310 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Restoring '%s' to '%s'" ),
1311 source.GetFullPath(), dest.GetFullPath() );
1312
1313 if( !wxRenameFile( source.GetFullPath(), dest.GetFullPath() ) )
1314 {
1315 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Failed to move '%s', rolling back" ),
1316 source.GetFullPath() );
1317 moveSuccess = false;
1318 break;
1319 }
1320
1321 restoredFiles.insert( filename );
1322 cont = tempDir.GetNext( &filename );
1323 }
1324 }
1325 }
1326
1327 // ROLLBACK if anything failed
1328 if( !moveSuccess )
1329 {
1330 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: ROLLING BACK due to failure" ) );
1331
1332 // Remove ONLY the files we successfully moved from temp directory
1333 // This preserves any files that were NOT in the backup (never tracked in history)
1334 for( const wxString& filename : restoredFiles )
1335 {
1336 wxFileName toRemove( aProjectPath, filename );
1337 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Rollback removing '%s'" ),
1338 toRemove.GetFullPath() );
1339
1340 if( toRemove.DirExists() )
1341 {
1342 wxFileName::Rmdir( toRemove.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1343 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Rollback removed directory '%s'" ),
1344 toRemove.GetFullPath() );
1345 }
1346 else if( toRemove.FileExists() )
1347 {
1348 wxRemoveFile( toRemove.GetFullPath() );
1349 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Rollback removed file '%s'" ),
1350 toRemove.GetFullPath() );
1351 }
1352 }
1353
1354 // Restore from backup - put back only what we moved
1355 if( wxDirExists( backupPath ) )
1356 {
1357 for( const wxString& filename : backedUpFiles )
1358 {
1359 wxFileName source( backupPath, filename );
1360 wxFileName dest( aProjectPath, filename );
1361
1362 if( source.Exists() )
1363 {
1364 wxRenameFile( source.GetFullPath(), dest.GetFullPath() );
1365 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Rollback restored '%s'" ),
1366 dest.GetFullPath() );
1367 }
1368 }
1369 }
1370
1371 // Clean up
1372 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1373 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1374
1375 git_tree_free( tree );
1376 git_commit_free( commit );
1377 return false;
1378 }
1379
1380 // SUCCESS - Clean up temp and backup directories
1381 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Restore successful, cleaning up" ) );
1382 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
1383 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
1384
1385 // Record the restore in history
1386 git_time_t t = git_commit_time( commit );
1387 wxDateTime dt( (time_t) t );
1388 git_signature* sig = nullptr;
1389 git_signature_now( &sig, "KiCad", "[email protected]" );
1390 git_commit* parent = nullptr;
1391 git_oid parent_id;
1392
1393 if( git_reference_name_to_id( &parent_id, repo, "HEAD" ) == 0 )
1394 git_commit_lookup( &parent, repo, &parent_id );
1395
1396 wxString msg;
1397 msg.Printf( wxS( "Restored from %s %s" ), aHash, dt.FormatISOCombined().c_str() );
1398
1399 git_oid new_id;
1400 const git_commit* constParent = parent;
1401 git_commit_create( &new_id, repo, "HEAD", sig, sig, nullptr,
1402 msg.mb_str().data(), tree, parent ? 1 : 0,
1403 parent ? &constParent : nullptr );
1404
1405 if( parent )
1406 git_commit_free( parent );
1407 git_signature_free( sig );
1408 git_tree_free( tree );
1409 git_commit_free( commit );
1410
1411 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Complete" ) );
1412 return true;
1413}
1414
1415void LOCAL_HISTORY::ShowRestoreDialog( const wxString& aProjectPath, wxWindow* aParent )
1416{
1417 if( !HistoryExists( aProjectPath ) )
1418 return;
1419
1420 wxString hist = historyPath( aProjectPath );
1421 git_repository* repo = nullptr;
1422
1423 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1424 return;
1425
1426 git_revwalk* walk = nullptr;
1427 git_revwalk_new( &walk, repo );
1428 git_revwalk_push_head( walk );
1429
1430 std::vector<wxString> choices;
1431 std::vector<wxString> hashes;
1432 git_oid oid;
1433
1434 while( git_revwalk_next( &oid, walk ) == 0 )
1435 {
1436 git_commit* commit = nullptr;
1437 git_commit_lookup( &commit, repo, &oid );
1438
1439 git_time_t t = git_commit_time( commit );
1440 wxDateTime dt( (time_t) t );
1441 wxString line;
1442
1443 line.Printf( wxS( "%s %s" ), dt.FormatISOCombined().c_str(),
1444 wxString::FromUTF8( git_commit_summary( commit ) ) );
1445 choices.push_back( line );
1446 hashes.push_back( wxString::FromUTF8( git_oid_tostr_s( &oid ) ) );
1447 git_commit_free( commit );
1448 }
1449
1450 git_revwalk_free( walk );
1451 git_repository_free( repo );
1452
1453 if( choices.empty() )
1454 return;
1455
1456 int index = wxGetSingleChoiceIndex( _( "Select snapshot" ), _( "Restore" ),
1457 (int) choices.size(), &choices[0], aParent );
1458
1459 if( index != wxNOT_FOUND )
1460 RestoreCommit( aProjectPath, hashes[index] );
1461}
1462
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.
static bool TagSave(const wxString &aProjectPath, const wxString &aFileType)
Tag a manual save in the local history repository.
static wxString GetHeadHash(const wxString &aProjectPath)
Return the current head commit hash.
static void ShowRestoreDialog(const wxString &aProjectPath, wxWindow *aParent)
Show a dialog allowing the user to choose a snapshot to restore.
static bool HeadNewerThanLastSave(const wxString &aProjectPath)
Return true if the autosave data is newer than the last manual save.
static 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...
static bool EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
static bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
static bool CommitSnapshot(const std::vector< wxString > &aFiles, const wxString &aTitle)
Commit the given files to the local history repository.
static bool RunRegisteredSaversAndCommit(const wxString &aProjectPath, const wxString &aTitle)
Run all registered savers and, if any staged changes differ from HEAD, create a commit.
static void NoteFileChange(const wxString &aFile)
Record that a file has been modified and should be included in the next snapshot.
static bool CommitPending()
Commit any pending modified files to the history repository.
static bool HistoryExists(const wxString &aProjectPath)
Return true if history exists for the project.
static bool CommitFullProjectSnapshot(const wxString &aProjectPath, const wxString &aTitle)
Commit a snapshot of the entire project directory (excluding the .history directory and ignored trans...
static bool RestoreCommit(const wxString &aProjectPath, const wxString &aHash)
Restore the project files to the state recorded by the given commit hash.
static void RegisterSaver(const std::function< void(const wxString &, std::vector< wxString > &)> &aSaver)
Register a saver callback invoked during autosave history commits.
bool Valid() const
Definition lockfile.h:254
bool IsLockedByMe()
Definition lockfile.h:227
#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)
static std::vector< std::function< void(const wxString &, std::vector< wxString > &)> > s_savers
static std::set< wxString > s_pendingFiles
File locking utilities.
PGM_BASE & Pgm()
The global program "get" accessor.
Definition pgm_base.cpp:946
see class PGM_BASE
int delta
wxLogTrace helper definitions.