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, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <local_history.h>
22#include <history_lock.h>
23#include <paths.h>
25#include <lockfile.h>
28#include <pgm_base.h>
29#include <thread_pool.h>
30#include <trace_helpers.h>
32#include <confirm.h>
33#include <progress_reporter.h>
34
35#include <kiplatform/io.h>
36
37#include <git2.h>
38#include <wx/filename.h>
39#include <wx/filefn.h>
40#include <wx/ffile.h>
41#include <wx/dir.h>
42#include <wx/datetime.h>
43#include <wx/log.h>
44#include <wx/msgdlg.h>
45
46#include <vector>
47#include <string>
48#include <memory>
49#include <algorithm>
50#include <set>
51#include <map>
52#include <functional>
53#include <cstring>
54
55// Resolve the local-history storage directory for @p aProjectPath, honoring the
56static wxString historyPath( const wxString& aProjectPath )
57{
58 return Pgm().GetSettingsManager().GetLocalHistoryDirForPath( aProjectPath );
59}
60
61
62// Join a saver-supplied relative path with the on-disk storage root for the
63// active backup format and location. Forward slashes in @p aRelativePath are
64// preserved so libgit2 paths remain platform-neutral.
65static wxString joinHistoryDestination( const wxString& aHistoryRoot,
66 const wxString& aRelativePath )
67{
68 wxFileName fn( aRelativePath );
69
70 if( fn.IsAbsolute() )
71 return fn.GetFullPath(); // Defensive: should not happen with the new contract.
72
73 // Prepend the history root while preserving any subdirectories supplied by the saver
74 // (e.g. hierarchical sheet "sub/sheet.kicad_sch" must land at
75 // "<root>/sub/sheet.kicad_sch", not "<root>/sheet.kicad_sch").
76 wxArrayString dirs = fn.GetDirs();
77
78 wxFileName dst;
79 dst.AssignDir( aHistoryRoot );
80
81 for( const wxString& d : dirs )
82 dst.AppendDir( d );
83
84 dst.SetFullName( fn.GetFullName() );
85 return dst.GetFullPath();
86}
87
88
89static const wxString AUTOSAVE_PREFIX = wxS( "_autosave-" );
90
91
92// Compare two files byte-for-byte.
93static bool filesContentEqual( const wxString& aPathA, const wxString& aPathB )
94{
95 wxFFile fileA( aPathA, wxS( "rb" ) );
96 wxFFile fileB( aPathB, wxS( "rb" ) );
97
98 if( !fileA.IsOpened() || !fileB.IsOpened() )
99 return false;
100
101 wxFileOffset lenA = fileA.Length();
102 wxFileOffset lenB = fileB.Length();
103
104 if( lenA < 0 || lenB < 0 || lenA != lenB )
105 return false;
106
107 constexpr size_t chunkSize = 64 * 1024;
108 std::vector<char> bufA( chunkSize );
109 std::vector<char> bufB( chunkSize );
110
111 while( !fileA.Eof() )
112 {
113 size_t readA = fileA.Read( bufA.data(), chunkSize );
114 size_t readB = fileB.Read( bufB.data(), chunkSize );
115
116 if( readA != readB )
117 return false;
118
119 if( readA > 0 && std::memcmp( bufA.data(), bufB.data(), readA ) != 0 )
120 return false;
121
122 if( fileA.Error() || fileB.Error() )
123 return false;
124 }
125
126 return true;
127}
128
129
130// Resolve the autosave-file destination for a given relative path. In PROJECT_DIR
131// mode the file lives next to the original (or under the same subdir for nested
132// schematic sheets) with an "_autosave-" prefix on the basename. In USER_DIR mode
133// the file mirrors the project tree under the user data root with no name munging
134// -- the per-project hash subdirectory already isolates autosave content.
135static wxString resolveAutosaveDestination( const wxString& aAutosaveRoot,
136 const wxString& aRelativePath,
137 BACKUP_LOCATION aLocation )
138{
139 wxFileName rel( aRelativePath );
140 wxFileName dst;
141 dst.AssignDir( aAutosaveRoot );
142
143 for( const wxString& d : rel.GetDirs() )
144 dst.AppendDir( d );
145
146 if( aLocation == BACKUP_LOCATION::PROJECT_DIR )
147 dst.SetFullName( AUTOSAVE_PREFIX + rel.GetFullName() );
148 else
149 dst.SetFullName( rel.GetFullName() );
150
151 return dst.GetFullPath();
152}
153
154
155// Compute the source-file path that an autosave destination corresponds to.
156// In PROJECT_DIR mode the source is the same directory minus the "_autosave-"
157// prefix. In USER_DIR mode the source is the original under the project tree.
158static wxString sourceForAutosaveFile( const wxString& aAutosavePath,
159 const wxString& aProjectPath,
160 const wxString& aAutosaveRoot,
161 BACKUP_LOCATION aLocation )
162{
163 wxFileName autosave( aAutosavePath );
164
165 if( aLocation == BACKUP_LOCATION::PROJECT_DIR )
166 {
167 wxString name = autosave.GetFullName();
168
169 if( !name.StartsWith( AUTOSAVE_PREFIX ) )
170 return wxEmptyString;
171
172 autosave.SetFullName( name.Mid( AUTOSAVE_PREFIX.length() ) );
173 return autosave.GetFullPath();
174 }
175
176 if( !aAutosavePath.StartsWith( aAutosaveRoot ) )
177 return wxEmptyString;
178
179 wxString rel = aAutosavePath.Mid( aAutosaveRoot.length() );
180 wxFileName projFn( aProjectPath, wxEmptyString );
181
182 return projFn.GetPathWithSep() + rel;
183}
184
185
186static bool commitSnapshotForProject( const wxString& aProjectPath, const std::vector<wxString>& aFiles,
187 const wxString& aTitle );
188
189
190// Single point of control: incremental git history is active only when the user
191// has selected BACKUP_FORMAT::INCREMENTAL. In zip mode we leave any pre-existing
192// .history dormant on disk and skip all write/commit operations so we do not
193// keep extending a history the user has switched off. Read-only paths
194// (HistoryExists, RestoreCommit, ShowRestoreDialog) intentionally bypass this
195// gate so users can still browse dormant history after switching back.
200
201
202// Local history is project-scoped. When pcbnew or eeschema is launched
203// standalone without a project, save paths can land anywhere on the
204// filesystem (e.g. /tmp), and walking those directories to feed libgit2
205// would be catastrophic.
206static bool isProjectDirectory( const wxString& aProjectPath )
207{
208 if( aProjectPath.IsEmpty() || !wxDirExists( aProjectPath ) )
209 return false;
210
211 wxDir dir( aProjectPath );
212 wxString name;
213
214 return dir.IsOpened()
215 && dir.GetFirst( &name, wxString( wxS( "*." ) ) + FILEEXT::ProjectFileExtension, wxDIR_FILES );
216}
217
218
219// Top-level project entries that must survive a restore unchanged: git/history metadata, the
220// transient restore staging directories (current and any timestamped retained copies), and
221// the per-project zip backup directory produced by SETTINGS_MANAGER::BackupProject (named
222// "<projectname>-backups").
223static bool isRestoreProtectedEntry( const wxString& aName )
224{
225 return aName == wxS( ".history" ) || aName == wxS( ".git" ) || aName == wxS( "_restore_backup" )
226 || aName.StartsWith( wxS( "_restore_backup_" ) ) || aName == wxS( "_restore_temp" )
227 || aName == wxS( "_restore_discard" ) || aName.EndsWith( PROJECT_BACKUPS_DIR_SUFFIX );
228}
229
233
238
239void LOCAL_HISTORY::NoteFileChange( const wxString& aFile )
240{
241 wxFileName fn( aFile );
242
243 if( fn.GetFullName() == wxS( "fp-info-cache" ) || !Pgm().GetCommonSettings()->m_Backup.enabled )
244 return;
245
246 m_pendingFiles.insert( fn.GetFullPath() );
247}
248
249
251 const void* aSaverObject,
252 const std::function<void( const wxString&, std::vector<HISTORY_FILE_DATA>& )>& aSaver )
253{
254 if( m_savers.find( aSaverObject ) != m_savers.end() )
255 {
256 wxLogTrace( traceAutoSave, wxS( "[history] Saver %p already registered, skipping" ), aSaverObject );
257 return;
258 }
259
260 m_savers[aSaverObject] = aSaver;
261 wxLogTrace( traceAutoSave, wxS( "[history] Registered saver %p (total=%zu)" ), aSaverObject, m_savers.size() );
262}
263
264
265void LOCAL_HISTORY::UnregisterSaver( const void* aSaverObject )
266{
268
269 auto it = m_savers.find( aSaverObject );
270
271 if( it != m_savers.end() )
272 {
273 m_savers.erase( it );
274 wxLogTrace( traceAutoSave, wxS( "[history] Unregistered saver %p (total=%zu)" ),
275 aSaverObject, m_savers.size() );
276 }
277}
278
279
281{
283 m_savers.clear();
284 wxLogTrace( traceAutoSave, wxS( "[history] Cleared all savers" ) );
285}
286
287
288bool LOCAL_HISTORY::RunRegisteredSaversAndCommit( const wxString& aProjectPath, const wxString& aTitle,
289 const wxString& aTagFileType )
290{
291 if( !Pgm().GetCommonSettings()->m_Backup.enabled )
292 {
293 wxLogTrace( traceAutoSave, wxS( "Autosave disabled, returning" ) );
294 return true;
295 }
296
298 {
299 wxLogTrace( traceAutoSave, wxS( "[history] Backup format is ZIP; skipping git commit" ) );
300 return true;
301 }
302
303 if( !isProjectDirectory( aProjectPath ) )
304 return false;
305
306 Init( aProjectPath );
307
308 wxLogTrace( traceAutoSave,
309 wxS( "[history] RunRegisteredSaversAndCommit start project='%s' title='%s' savers=%zu tag='%s'" ),
310 aProjectPath, aTitle, m_savers.size(), aTagFileType );
311
312 if( m_savers.empty() )
313 {
314 wxLogTrace( traceAutoSave, wxS( "[history] no savers registered; skipping") );
315 return false;
316 }
317
318 // Manual save must land; autosave is droppable because another tick will retry.
319 if( !aTagFileType.IsEmpty() )
320 {
322 }
323 else if( m_saveInProgress.load( std::memory_order_acquire ) )
324 {
325 wxLogTrace( traceAutoSave, wxS( "[history] previous save still in progress; skipping cycle" ) );
326 return false;
327 }
328
329 // Phase 1 (UI thread): call savers to collect serialized data
330 std::vector<HISTORY_FILE_DATA> fileData;
331
332 for( const auto& [saverObject, saver] : m_savers )
333 {
334 size_t before = fileData.size();
335 saver( aProjectPath, fileData );
336 wxLogTrace( traceAutoSave, wxS( "[history] saver %p produced %zu entries (total=%zu)" ),
337 saverObject, fileData.size() - before, fileData.size() );
338 }
339
340 // Reject entries with an empty or absolute relativePath; the saver contract requires
341 // a project-relative path so we can dispatch to either the .history mirror or the
342 // autosave-files root without ambiguity.
343 fileData.erase( std::remove_if( fileData.begin(), fileData.end(),
344 []( const HISTORY_FILE_DATA& entry )
345 {
346 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
347 {
348 wxLogTrace( traceAutoSave, wxS( "[history] filtered out entry with invalid path: '%s'" ),
349 entry.relativePath );
350 return true;
351 }
352 return false;
353 } ),
354 fileData.end() );
355
356 if( fileData.empty() )
357 {
358 wxLogTrace( traceAutoSave, wxS( "[history] saver set produced no entries; skipping" ) );
359 return false;
360 }
361
362 // Phase 2: submit Prettify + file I/O + git to background thread
363 m_saveInProgress.store( true, std::memory_order_release );
364
365 m_pendingFuture = GetKiCadThreadPool().submit_task(
366 [this, projectPath = aProjectPath, title = aTitle, tagFileType = aTagFileType,
367 data = std::move( fileData )]() mutable -> bool
368 {
369 bool result = commitInBackground( projectPath, title, data, !tagFileType.IsEmpty() );
370
371 if( !tagFileType.IsEmpty() )
372 TagSave( projectPath, tagFileType );
373
374 m_saveInProgress.store( false, std::memory_order_release );
375 return result;
376 } );
377
378 // Manual save must complete (commit + tag)
379 if( !aTagFileType.IsEmpty() )
380 WaitForPendingSave();
381
382 return true;
383}
384
385
386bool LOCAL_HISTORY::RunRegisteredSaversAsAutosaveFiles( const wxString& aProjectPath )
387{
388 if( !Pgm().GetCommonSettings()->m_Backup.enabled )
389 return true;
390
391 if( m_savers.empty() )
392 {
393 wxLogTrace( traceAutoSave, wxS( "[autosave] no savers registered; skipping" ) );
394 return false;
395 }
396
399 wxString autosaveRoot = mgr.GetAutosaveRootForProject( mgr.GetProjectForPath( aProjectPath ) );
400
401 if( !PATHS::EnsurePathExists( autosaveRoot ) )
402 {
403 wxLogTrace( traceAutoSave, wxS( "[autosave] cannot create autosave root '%s'" ), autosaveRoot );
404 return false;
405 }
406
407 std::vector<HISTORY_FILE_DATA> fileData;
408
409 for( const auto& [saverObject, saver] : m_savers )
410 saver( aProjectPath, fileData );
411
412 bool anyWritten = false;
413
414 for( HISTORY_FILE_DATA& entry : fileData )
415 {
416 if( entry.relativePath.IsEmpty() || wxFileName( entry.relativePath ).IsAbsolute() )
417 continue;
418
419 wxString dst = resolveAutosaveDestination( autosaveRoot, entry.relativePath, location );
420 wxFileName dstFn( dst );
421
422 if( !PATHS::EnsurePathExists( dstFn.GetPath() ) )
423 {
424 wxLogTrace( traceAutoSave, wxS( "[autosave] cannot create dir '%s'" ), dstFn.GetPath() );
425 continue;
426 }
427
428 std::string buf;
429
430 if( !entry.content.empty() )
431 {
432 buf = std::move( entry.content );
433
434 if( entry.prettify )
435 KICAD_FORMAT::Prettify( buf, entry.formatMode );
436 }
437 else if( !entry.sourcePath.IsEmpty() )
438 {
439 wxFFile src( entry.sourcePath, wxS( "rb" ) );
440
441 if( !src.IsOpened() )
442 continue;
443
444 wxFileOffset len = src.Length();
445
446 if( len < 0 )
447 continue;
448
449 buf.resize( static_cast<size_t>( len ) );
450
451 if( len > 0 && src.Read( buf.data(), buf.size() ) != buf.size() )
452 {
453 buf.clear();
454 continue;
455 }
456 }
457 else
458 {
459 continue;
460 }
461
462 wxString err;
463
464 if( KIPLATFORM::IO::AtomicWriteFile( dst, buf.data(), buf.size(), &err ) )
465 {
466 anyWritten = true;
467 wxLogTrace( traceAutoSave, wxS( "[autosave] wrote %zu bytes to '%s'" ), buf.size(), dst );
468 }
469 else
470 {
471 wxLogTrace( traceAutoSave, wxS( "[autosave] write failed for '%s': %s" ), dst, err );
472 }
473 }
474
475 return anyWritten;
476}
477
478
479// Enumerate every (autosave, source) pair under the per-project autosave root, without
480// any modification-time filter. Callers that want only files newer than their source
481// (the recovery-prompt path) apply that filter themselves; cleanup callers want the
482// full list so they can remove leftover autosave files even when the source has been
483// re-saved and is newer.
484static std::vector<std::pair<wxString, wxString>>
485findAutosaveFilePairs( const wxString& aProjectPath )
486{
487 std::vector<std::pair<wxString, wxString>> results;
488
491 wxString autosaveRoot = mgr.GetAutosaveRootForProject( mgr.GetProjectForPath( aProjectPath ) );
492
493 if( !wxDirExists( autosaveRoot ) )
494 return results;
495
496 std::function<void( const wxString& )> walk = [&]( const wxString& aDir )
497 {
498 wxDir d( aDir );
499
500 if( !d.IsOpened() )
501 return;
502
503 wxString name;
504 bool cont = d.GetFirst( &name );
505
506 while( cont )
507 {
508 wxFileName fn( aDir, name );
509 wxString fullPath = fn.GetFullPath();
510
511 if( wxDirExists( fullPath ) )
512 {
514 && ( name == wxS( ".history" ) || name.EndsWith( wxS( "-backups" ) ) ) )
515 {
516 cont = d.GetNext( &name );
517 continue;
518 }
519
520 walk( fullPath );
521 }
523 || fn.GetFullName().StartsWith( AUTOSAVE_PREFIX ) )
524 {
525 wxString src = sourceForAutosaveFile( fullPath, aProjectPath, autosaveRoot,
526 location );
527
528 if( !src.IsEmpty() )
529 results.emplace_back( fullPath, src );
530 }
531
532 cont = d.GetNext( &name );
533 }
534 };
535
536 walk( autosaveRoot );
537 return results;
538}
539
540
541std::vector<std::pair<wxString, wxString>>
542LOCAL_HISTORY::FindStaleAutosaveFiles( const wxString& aProjectPath, const std::vector<wxString>& aExtensions ) const
543{
544 std::vector<std::pair<wxString, wxString>> results;
545
546 if( aExtensions.empty() )
547 return results;
548
549 for( auto& pair : findAutosaveFilePairs( aProjectPath ) )
550 {
551 wxFileName srcFn( pair.second );
552 bool match = false;
553
554 for( const wxString& ext : aExtensions )
555 {
556 if( srcFn.GetExt().IsSameAs( ext, false ) )
557 {
558 match = true;
559 break;
560 }
561 }
562
563 if( !match )
564 continue;
565
566 wxDateTime srcTime;
567
568 if( srcFn.FileExists() )
569 srcTime = srcFn.GetModificationTime();
570
571 wxDateTime autosaveTime = wxFileName( pair.first ).GetModificationTime();
572
573 // mtime is only a pre-filter; cloud-sync clients bump the byte-identical autosave's
574 // mtime past the source, so confirm the content actually diverges (issue 24126).
575 bool stale = !srcTime.IsValid()
576 || ( autosaveTime.IsLaterThan( srcTime )
577 && !filesContentEqual( pair.first, pair.second ) );
578
579 if( stale )
580 results.emplace_back( std::move( pair ) );
581 }
582
583 return results;
584}
585
586
587void LOCAL_HISTORY::RemoveAutosaveFiles( const wxString& aProjectPath ) const
588{
589 // After a successful manual save the source typically has a newer mtime than its
590 // autosave, so we cannot rely on FindStaleAutosaveFiles() here -- we need to remove
591 // every autosave file associated with the project regardless of mtime.
592 for( const auto& [autosavePath, srcPath] : findAutosaveFilePairs( aProjectPath ) )
593 {
594 if( wxFileExists( autosavePath ) )
595 wxRemoveFile( autosavePath );
596 }
597}
598
599
600void LOCAL_HISTORY::RemoveAutosaveFiles( const wxString& aProjectPath,
601 const std::vector<wxString>& aSourcePaths ) const
602{
603 if( aSourcePaths.empty() )
604 return;
605
606 std::vector<wxFileName> targets;
607 targets.reserve( aSourcePaths.size() );
608
609 for( const wxString& src : aSourcePaths )
610 {
611 if( !src.IsEmpty() )
612 targets.emplace_back( src );
613 }
614
615 if( targets.empty() )
616 return;
617
618 for( const auto& [autosavePath, srcPath] : findAutosaveFilePairs( aProjectPath ) )
619 {
620 wxFileName srcFn( srcPath );
621 bool match = false;
622
623 for( const wxFileName& target : targets )
624 {
625 if( srcFn.SameAs( target ) )
626 {
627 match = true;
628 break;
629 }
630 }
631
632 if( match && wxFileExists( autosavePath ) )
633 wxRemoveFile( autosavePath );
634 }
635}
636
637
638bool LOCAL_HISTORY::commitInBackground( const wxString& aProjectPath, const wxString& aTitle,
639 const std::vector<HISTORY_FILE_DATA>& aFileData, bool aIsManualSave )
640{
641 wxLogTrace( traceAutoSave, wxS( "[history] background: writing %zu entries for '%s'" ),
642 aFileData.size(), aProjectPath );
643
644 wxString hist = historyPath( aProjectPath );
645
646 if( !PATHS::EnsurePathExists( hist ) )
647 {
648 wxLogTrace( traceAutoSave, wxS( "[history] background: cannot create history root '%s'" ), hist );
649 return false;
650 }
651
652 for( const HISTORY_FILE_DATA& entry : aFileData )
653 {
654 wxString dst = joinHistoryDestination( hist, entry.relativePath );
655 wxFileName dstFn( dst );
656 wxString parent = dstFn.GetPath();
657
658 if( !parent.IsEmpty() && !PATHS::EnsurePathExists( parent ) )
659 {
660 wxLogTrace( traceAutoSave, wxS( "[history] background: cannot create dir '%s'" ), parent );
661 continue;
662 }
663
664 if( !entry.content.empty() )
665 {
666 std::string buf = entry.content;
667
668 if( entry.prettify )
669 KICAD_FORMAT::Prettify( buf, entry.formatMode );
670
671 wxFFile fp( dst, wxS( "wb" ) );
672
673 if( fp.IsOpened() )
674 {
675 fp.Write( buf.data(), buf.size() );
676 fp.Close();
677 wxLogTrace( traceAutoSave, wxS( "[history] background: wrote %zu bytes to '%s'" ), buf.size(), dst );
678 }
679 else
680 {
681 wxLogTrace( traceAutoSave, wxS( "[history] background: failed to open '%s' for writing" ), dst );
682 }
683 }
684 else if( !entry.sourcePath.IsEmpty() )
685 {
686 wxCopyFile( entry.sourcePath, dst, true );
687 wxLogTrace( traceAutoSave, wxS( "[history] background: copied '%s' -> '%s'" ), entry.sourcePath, dst );
688 }
689 }
690
691 // Acquire locks using hybrid locking strategy
692 HISTORY_LOCK_MANAGER lock( aProjectPath );
693
694 if( !lock.IsLocked() )
695 {
696 wxLogTrace( traceAutoSave, wxS( "[history] background: failed to acquire lock: %s" ), lock.GetLockError() );
697 return false;
698 }
699
700 git_repository* repo = lock.GetRepository();
701 git_index* index = lock.GetIndex();
702
703 git_repository_set_workdir( repo, hist.mb_str().data(), false );
704
705 // Stage all written files using their project-relative paths. libgit2 needs forward
706 // slashes on every platform, so normalize before adding to the index.
707 for( const HISTORY_FILE_DATA& entry : aFileData )
708 {
709 wxString rel = entry.relativePath;
710 rel.Replace( wxS( "\\" ), wxS( "/" ) );
711
712 wxString abs = joinHistoryDestination( hist, entry.relativePath );
713
714 if( !wxFileExists( abs ) )
715 continue;
716
717 git_index_add_bypath( index, rel.ToStdString().c_str() );
718 }
719
720 // Compare index to HEAD; if no diff -> abort to avoid empty commit.
721 git_oid head_oid;
722 git_commit* head_commit = nullptr;
723 git_tree* head_tree = nullptr;
724
725 bool headExists = ( git_reference_name_to_id( &head_oid, repo, "HEAD" ) == 0 )
726 && ( git_commit_lookup( &head_commit, repo, &head_oid ) == 0 )
727 && ( git_commit_tree( &head_tree, head_commit ) == 0 );
728
729 git_tree* rawIndexTree = nullptr;
730 git_oid index_tree_oid;
731
732 if( git_index_write_tree( &index_tree_oid, index ) != 0 )
733 {
734 if( head_tree )
735 git_tree_free( head_tree );
736
737 if( head_commit )
738 git_commit_free( head_commit );
739
740 wxLogTrace( traceAutoSave, wxS("[history] background: failed to write index tree" ) );
741 return false;
742 }
743
744 git_tree_lookup( &rawIndexTree, repo, &index_tree_oid );
745 std::unique_ptr<git_tree, decltype( &git_tree_free )> indexTree( rawIndexTree, &git_tree_free );
746
747 bool hasChanges = true;
748
749 if( headExists )
750 {
751 git_diff* diff = nullptr;
752
753 if( git_diff_tree_to_tree( &diff, repo, head_tree, indexTree.get(), nullptr ) == 0 )
754 {
755 hasChanges = git_diff_num_deltas( diff ) > 0;
756 wxLogTrace( traceAutoSave, wxS( "[history] background: diff deltas=%u" ),
757 (unsigned) git_diff_num_deltas( diff ) );
758 git_diff_free( diff );
759 }
760 }
761 else
762 {
763 // No HEAD: skip commit if staged matches disk, so an idle autosave on a fresh
764 // project doesn't leave an untagged HEAD that triggers a no-op restore prompt.
765 bool stagedMatchesDisk = true;
766
767 for( const HISTORY_FILE_DATA& entry : aFileData )
768 {
769 wxString diskPath = aProjectPath + wxFileName::GetPathSeparator() + entry.relativePath;
770 wxString histPath = joinHistoryDestination( hist, entry.relativePath );
771
772 if( !wxFileExists( diskPath ) || !wxFileExists( histPath ) )
773 {
774 stagedMatchesDisk = false;
775 break;
776 }
777
778 wxFFile diskFile( diskPath, wxT( "rb" ) );
779 wxFFile histFile( histPath, wxT( "rb" ) );
780
781 if( !diskFile.IsOpened() || !histFile.IsOpened() || diskFile.Length() != histFile.Length() )
782 {
783 stagedMatchesDisk = false;
784 break;
785 }
786
787 size_t len = static_cast<size_t>( diskFile.Length() );
788 std::string diskBuf( len, '\0' );
789 std::string histBuf( len, '\0' );
790
791 if( diskFile.Read( diskBuf.data(), len ) != len
792 || histFile.Read( histBuf.data(), len ) != len
793 || diskBuf != histBuf )
794 {
795 stagedMatchesDisk = false;
796 break;
797 }
798 }
799
800 if( stagedMatchesDisk && !aIsManualSave )
801 {
802 wxLogTrace( traceAutoSave, wxS( "[history] background: first commit; staged matches disk -- skipping" ) );
803 hasChanges = false;
804 }
805 }
806
807 if( head_tree )
808 git_tree_free( head_tree );
809
810 if( head_commit )
811 git_commit_free( head_commit );
812
813 if( !hasChanges )
814 {
815 wxLogTrace( traceAutoSave, wxS("[history] background: no changes detected; no commit") );
816
817 // Manual save matching HEAD: amend the prior message so the user's explicit save
818 // shows in the history dialog. Skip if HEAD already has this title.
819 if( !aTitle.IsEmpty() && aTitle != wxS( "Autosave" ) )
820 {
821 git_oid head_oid_amend;
822
823 if( git_reference_name_to_id( &head_oid_amend, repo, "HEAD" ) == 0 )
824 {
825 git_commit* head_commit_amend = nullptr;
826
827 if( git_commit_lookup( &head_commit_amend, repo, &head_oid_amend ) == 0 )
828 {
829 wxString existingMsg = wxString::FromUTF8( git_commit_message( head_commit_amend ) );
830 existingMsg.Trim( true ).Trim( false );
831
832 if( existingMsg != aTitle )
833 {
834 git_oid amended_oid;
835 int amend_rc = git_commit_amend( &amended_oid, head_commit_amend, "HEAD", nullptr, nullptr,
836 nullptr, aTitle.mb_str().data(), nullptr );
837
838 if( amend_rc == 0 )
839 wxLogTrace( traceAutoSave, wxS( "[history] background: amended HEAD message '%s' -> '%s'" ),
840 existingMsg, aTitle );
841 else
842 wxLogTrace( traceAutoSave, wxS( "[history] background: amend failed rc=%d" ), amend_rc );
843 }
844
845 git_commit_free( head_commit_amend );
846 }
847 }
848 }
849
850 return false; // Nothing new; skip commit.
851 }
852
853 git_signature* rawSig = nullptr;
854 git_signature_now( &rawSig, "KiCad", "[email protected]" );
855 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( rawSig, &git_signature_free );
856
857 git_commit* parent = nullptr;
858 git_oid parent_id;
859 int parents = 0;
860
861 if( git_reference_name_to_id( &parent_id, repo, "HEAD" ) == 0 )
862 {
863 if( git_commit_lookup( &parent, repo, &parent_id ) == 0 )
864 parents = 1;
865 }
866
867 wxString msg = aTitle.IsEmpty() ? wxString( "Autosave" ) : aTitle;
868 git_oid commit_id;
869 const git_commit* constParent = parent;
870
871 int rc = git_commit_create( &commit_id, repo, "HEAD", sig.get(), sig.get(), nullptr,
872 msg.mb_str().data(), indexTree.get(), parents,
873 parents ? &constParent : nullptr );
874
875 if( rc == 0 )
876 {
877 wxLogTrace( traceAutoSave, wxS( "[history] background: commit created %s (%s entries=%zu)" ),
878 wxString::FromUTF8( git_oid_tostr_s( &commit_id ) ), msg, aFileData.size() );
879 }
880 else
881 {
882 wxLogTrace( traceAutoSave, wxS( "[history] background: commit failed rc=%d" ), rc );
883 }
884
885 if( parent )
886 git_commit_free( parent );
887
888 git_index_write( index );
889 return rc == 0;
890}
891
892
894{
895 if( m_pendingFuture.valid() )
896 {
897 wxLogTrace( traceAutoSave, wxS( "[history] waiting for pending background save" ) );
898 m_pendingFuture.get();
899 }
900}
901
902
904{
905 std::vector<wxString> files( m_pendingFiles.begin(), m_pendingFiles.end() );
906 m_pendingFiles.clear();
907 return CommitSnapshot( files, wxS( "Autosave" ) );
908}
909
910
911bool LOCAL_HISTORY::Init( const wxString& aProjectPath )
912{
913 if( !isProjectDirectory( aProjectPath ) )
914 return false;
915
916 if( !Pgm().GetCommonSettings()->m_Backup.enabled || !formatUsesIncrementalHistory() )
917 return true;
918
919 wxString hist = historyPath( aProjectPath );
920
921 if( !wxDirExists( hist ) )
922 {
923 // EnsurePathExists creates intermediate directories as needed, which is required
924 // for USER_DIR mode where the parent (e.g., ~/.config/kicad/<ver>/local_history/)
925 // may not yet exist. In PROJECT_DIR mode it falls back to a single mkdir.
926 if( !PATHS::EnsurePathExists( hist ) )
927 return false;
928 }
929
930 git_repository* rawRepo = nullptr;
931
932 if( git_repository_open( &rawRepo, hist.mb_str().data() ) != 0 )
933 {
934 if( git_repository_init( &rawRepo, hist.mb_str().data(), 0 ) != 0 )
935 return false;
936
937 wxFileName ignoreFile( hist, wxS( ".gitignore" ) );
938 if( !ignoreFile.FileExists() )
939 {
940 wxFFile f( ignoreFile.GetFullPath(), wxT( "w" ) );
941 if( f.IsOpened() )
942 {
943 f.Write( wxS( "# KiCad local history exclusions. Edit to add your own rules.\n"
944 "fp-info-cache\n"
945 "*-backups/\n" ) );
946 f.Close();
947 }
948 }
949
950 wxFileName readmeFile( hist, wxS( "README.txt" ) );
951
952 if( !readmeFile.FileExists() )
953 {
954 wxFFile f( readmeFile.GetFullPath(), wxT( "w" ) );
955
956 if( f.IsOpened() )
957 {
958 f.Write( wxS( "KiCad Local History Directory\n"
959 "=============================\n\n"
960 "This directory contains automatic snapshots of your project files.\n"
961 "KiCad periodically saves copies of your work here, allowing you to\n"
962 "recover from accidental changes or data loss.\n\n"
963 "You can browse and restore previous versions through KiCad's\n"
964 "File > Local History menu.\n\n"
965 "To disable this feature:\n"
966 " Preferences > Common > Project Backup > Enable automatic backups\n\n"
967 "This directory can be safely deleted if you no longer need the\n"
968 "history, but doing so will permanently remove all saved snapshots.\n" ) );
969 f.Close();
970 }
971 }
972 }
973
974 git_repository_free( rawRepo );
975
976 return true;
977}
978
979
980// Helper function to commit files using an already-acquired lock
987
988
989static SNAPSHOT_COMMIT_RESULT commitSnapshotWithLock( git_repository* repo, git_index* index,
990 const wxString& aHistoryPath, const wxString& aProjectPath,
991 const std::vector<wxString>& aFiles, const wxString& aTitle )
992{
993 std::vector<std::string> filesArrStr;
994
995 for( const wxString& file : aFiles )
996 {
997 wxFileName src( file );
998 wxString relPath;
999
1000 if( src.GetFullPath().StartsWith( aProjectPath + wxFILE_SEP_PATH ) )
1001 relPath = src.GetFullPath().Mid( aProjectPath.length() + 1 );
1002 else
1003 relPath = src.GetFullName(); // Fallback (should not normally happen)
1004
1005 relPath.Replace( "\\", "/" ); // libgit2 needs forward slashes on all platforms
1006 std::string relPathStr = relPath.ToStdString();
1007
1008 unsigned int status = 0;
1009 int rc = git_status_file( &status, repo, relPathStr.data() );
1010
1011 if( rc == 0 && status != 0 )
1012 {
1013 wxLogTrace( traceAutoSave, wxS( "File %s status %d " ), relPath, status );
1014 filesArrStr.emplace_back( relPathStr );
1015 }
1016 else if( rc != 0 )
1017 {
1018 wxLogTrace( traceAutoSave, wxS( "File %s status error %d " ), relPath, rc );
1019 filesArrStr.emplace_back( relPathStr ); // Add anyway even if the file is untracked.
1020 }
1021 }
1022
1023 std::vector<char*> cStrings( filesArrStr.size() );
1024
1025 for( size_t i = 0; i < filesArrStr.size(); i++ )
1026 cStrings[i] = filesArrStr[i].data();
1027
1028 git_strarray filesArrGit;
1029 filesArrGit.count = filesArrStr.size();
1030 filesArrGit.strings = cStrings.data();
1031
1032 if( filesArrStr.size() == 0 )
1033 {
1034 wxLogTrace( traceAutoSave, wxS( "No changes, skipping" ) );
1036 }
1037
1038 int rc = git_index_add_all( index, &filesArrGit, GIT_INDEX_ADD_DISABLE_PATHSPEC_MATCH | GIT_INDEX_ADD_FORCE, NULL,
1039 NULL );
1040 wxLogTrace( traceAutoSave, wxS( "Adding %zu files, rc %d" ), filesArrStr.size(), rc );
1041
1042 if( rc != 0 )
1044
1045 git_oid tree_id;
1046 if( git_index_write_tree( &tree_id, index ) != 0 )
1048
1049 git_tree* rawTree = nullptr;
1050 git_tree_lookup( &rawTree, repo, &tree_id );
1051 std::unique_ptr<git_tree, decltype( &git_tree_free )> tree( rawTree, &git_tree_free );
1052
1053 git_signature* rawSig = nullptr;
1054 git_signature_now( &rawSig, "KiCad", "[email protected]" );
1055 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( rawSig,
1056 &git_signature_free );
1057
1058 git_commit* rawParent = nullptr;
1059 git_oid parent_id;
1060 int parents = 0;
1061
1062 if( git_reference_name_to_id( &parent_id, repo, "HEAD" ) == 0 )
1063 {
1064 git_commit_lookup( &rawParent, repo, &parent_id );
1065 parents = 1;
1066 }
1067
1068 std::unique_ptr<git_commit, decltype( &git_commit_free )> parent( rawParent,
1069 &git_commit_free );
1070
1071 git_tree* rawParentTree = nullptr;
1072
1073 if( parent )
1074 git_commit_tree( &rawParentTree, parent.get() );
1075
1076 std::unique_ptr<git_tree, decltype( &git_tree_free )> parentTree( rawParentTree, &git_tree_free );
1077
1078 git_diff* rawDiff = nullptr;
1079 git_diff_tree_to_index( &rawDiff, repo, parentTree.get(), index, nullptr );
1080 std::unique_ptr<git_diff, decltype( &git_diff_free )> diff( rawDiff, &git_diff_free );
1081
1082 size_t numChangedFiles = git_diff_num_deltas( diff.get() );
1083
1084 if( numChangedFiles == 0 )
1085 {
1086 wxLogTrace( traceAutoSave, wxS( "No actual changes in tree, skipping commit" ) );
1088 }
1089
1090 wxString msg;
1091
1092 if( !aTitle.IsEmpty() )
1093 msg << aTitle << wxS( ": " );
1094
1095 msg << numChangedFiles << wxS( " files changed" );
1096
1097 for( size_t i = 0; i < numChangedFiles; ++i )
1098 {
1099 const git_diff_delta* delta = git_diff_get_delta( diff.get(), i );
1100 git_patch* rawPatch = nullptr;
1101 git_patch_from_diff( &rawPatch, diff.get(), i );
1102 std::unique_ptr<git_patch, decltype( &git_patch_free )> patch( rawPatch,
1103 &git_patch_free );
1104 size_t context = 0, adds = 0, dels = 0;
1105 git_patch_line_stats( &context, &adds, &dels, patch.get() );
1106 size_t updated = std::min( adds, dels );
1107 adds -= updated;
1108 dels -= updated;
1109 msg << wxS( "\n" ) << wxString::FromUTF8( delta->new_file.path )
1110 << wxS( " " ) << adds << wxS( "/" ) << dels << wxS( "/" ) << updated;
1111 }
1112
1113 git_oid commit_id;
1114 git_commit* parentPtr = parent.get();
1115 const git_commit* constParentPtr = parentPtr;
1116 if( git_commit_create( &commit_id, repo, "HEAD", sig.get(), sig.get(), nullptr, msg.mb_str().data(), tree.get(),
1117 parents, parentPtr ? &constParentPtr : nullptr )
1118 != 0 )
1119 {
1121 }
1122
1123 git_index_write( index );
1125}
1126
1127
1128// Internal entry point used when the project root is already known. The public
1129// CommitSnapshot() derives the project from aFiles[0], which is unsafe when the
1130// caller has collected files recursively (the first entry can live in a subdirectory).
1131static bool commitSnapshotForProject( const wxString& aProjectPath, const std::vector<wxString>& aFiles,
1132 const wxString& aTitle )
1133{
1134 wxString hist = historyPath( aProjectPath );
1135
1136 HISTORY_LOCK_MANAGER lock( aProjectPath );
1137
1138 if( !lock.IsLocked() )
1139 {
1140 wxLogTrace( traceAutoSave, wxS( "[history] commitSnapshotForProject failed to acquire lock: %s" ),
1141 lock.GetLockError() );
1142 return false;
1143 }
1144
1145 return commitSnapshotWithLock( lock.GetRepository(), lock.GetIndex(), hist, aProjectPath, aFiles, aTitle )
1147}
1148
1149
1150bool LOCAL_HISTORY::CommitSnapshot( const std::vector<wxString>& aFiles, const wxString& aTitle )
1151{
1152 if( aFiles.empty() || !Pgm().GetCommonSettings()->m_Backup.enabled
1154 {
1155 return true;
1156 }
1157
1158 wxString proj = wxFileName( aFiles[0] ).GetPath();
1159
1160 if( !isProjectDirectory( proj ) )
1161 return false;
1162
1163 Init( proj );
1164 return commitSnapshotForProject( proj, aFiles, aTitle );
1165}
1166
1167
1168// Limit snapshots to KiCad project artifacts (kicad_* extensions and the no-extension
1169// lib-tables) so unrelated files in the project dir don't end up in .history.
1170static bool isKiCadProjectFile( const wxFileName& aFile )
1171{
1172 wxString name = aFile.GetFullName();
1173
1174 if( name == wxS( "sym-lib-table" ) || name == wxS( "fp-lib-table" ) )
1175 return true;
1176
1177 return aFile.GetExt().StartsWith( wxS( "kicad_" ) );
1178}
1179
1180
1181// Helper to collect KiCad project files (excluding .history, backups, transient caches,
1182// and any non-KiCad files such as user PDFs or notes).
1183// Skips subtrees that contain a kicad_pro file since they belong to nested projects.
1184static void collectProjectFiles( const wxString& aProjectPath, std::vector<wxString>& aFiles )
1185{
1186 wxDir dir( aProjectPath );
1187
1188 if( !dir.IsOpened() )
1189 return;
1190
1191 // Collect recursively. Flag top-level to avoid hitting the same logic for nested projects
1192 std::function<void( const wxString&, bool )> collect =
1193 [&]( const wxString& path, bool topLevel )
1194 {
1195 if( !topLevel && isProjectDirectory( path ) )
1196 {
1197 wxLogTrace( traceAutoSave,
1198 wxS( "[history] collectProjectFiles: Skipping nested project at %s" ),
1199 path );
1200 return;
1201 }
1202
1203 wxString name;
1204 wxDir d( path );
1205
1206 if( !d.IsOpened() )
1207 return;
1208
1209 bool cont = d.GetFirst( &name );
1210
1211 while( cont )
1212 {
1213 if( topLevel && isRestoreProtectedEntry( name ) )
1214 {
1215 cont = d.GetNext( &name );
1216 continue;
1217 }
1218
1219 wxFileName fn( path, name );
1220 wxString fullPath = fn.GetFullPath();
1221
1222 if( wxFileName::DirExists( fullPath ) )
1223 {
1224 collect( fullPath, false );
1225 }
1226 else if( fn.FileExists() && fn.GetFullName() != wxS( "fp-info-cache" ) && isKiCadProjectFile( fn ) )
1227 {
1228 aFiles.push_back( fn.GetFullPath() );
1229 }
1230
1231 cont = d.GetNext( &name );
1232 }
1233 };
1234
1235 collect( aProjectPath, true );
1236}
1237
1238
1239bool LOCAL_HISTORY::CommitFullProjectSnapshot( const wxString& aProjectPath, const wxString& aTitle )
1240{
1241 if( !isProjectDirectory( aProjectPath ) || !Pgm().GetCommonSettings()->m_Backup.enabled )
1242 return false;
1243
1245 {
1246 wxLogTrace( traceAutoSave, wxS("[history] Backup format is ZIP; skipping full snapshot" ) );
1247 return true;
1248 }
1249
1250 std::vector<wxString> files;
1251 collectProjectFiles( aProjectPath, files );
1252
1253 if( files.empty() )
1254 return false;
1255
1256 Init( aProjectPath );
1257 return commitSnapshotForProject( aProjectPath, files, aTitle );
1258}
1259
1260bool LOCAL_HISTORY::HistoryExists( const wxString& aProjectPath )
1261{
1262 return wxDirExists( historyPath( aProjectPath ) );
1263}
1264
1265// Add a Save_<type>_N tag and move Last_Save_<type> to the current HEAD using an already-open
1266// repo. Shared by TagSave and the restore path, the latter holds the history lock itself and so
1267// cannot go through TagSave (which would try to re-acquire it).
1268static bool tagSaveAtHead( git_repository* repo, const wxString& aFileType )
1269{
1270 if( !repo )
1271 return false;
1272
1273 git_oid head;
1274 if( git_reference_name_to_id( &head, repo, "HEAD" ) != 0 )
1275 return false;
1276
1277 wxString tagName;
1278 int i = 1;
1279 git_reference* ref = nullptr;
1280 do
1281 {
1282 tagName.Printf( wxS( "Save_%s_%d" ), aFileType, i++ );
1283 } while( git_reference_lookup( &ref, repo, ( wxS( "refs/tags/" ) + tagName ).mb_str().data() ) == 0 );
1284
1285 git_oid tag_oid;
1286 git_object* head_obj = nullptr;
1287 git_object_lookup( &head_obj, repo, &head, GIT_OBJECT_COMMIT );
1288 git_tag_create_lightweight( &tag_oid, repo, tagName.mb_str().data(), head_obj, 0 );
1289 git_object_free( head_obj );
1290
1291 wxString lastName;
1292 lastName.Printf( wxS( "Last_Save_%s" ), aFileType );
1293 if( git_reference_lookup( &ref, repo, ( wxS( "refs/tags/" ) + lastName ).mb_str().data() ) == 0 )
1294 {
1295 git_reference_delete( ref );
1296 git_reference_free( ref );
1297 }
1298
1299 git_oid last_tag_oid;
1300 git_object* head_obj2 = nullptr;
1301 git_object_lookup( &head_obj2, repo, &head, GIT_OBJECT_COMMIT );
1302 git_tag_create_lightweight( &last_tag_oid, repo, lastName.mb_str().data(), head_obj2, 0 );
1303 git_object_free( head_obj2 );
1304
1305 return true;
1306}
1307
1308
1309bool LOCAL_HISTORY::TagSave( const wxString& aProjectPath, const wxString& aFileType )
1310{
1311 if( !Pgm().GetCommonSettings()->m_Backup.enabled || !formatUsesIncrementalHistory() )
1312 return true;
1313
1314 if( !isProjectDirectory( aProjectPath ) )
1315 return false;
1316
1317 HISTORY_LOCK_MANAGER lock( aProjectPath );
1318
1319 if( !lock.IsLocked() )
1320 {
1321 wxLogTrace( traceAutoSave, wxS( "[history] TagSave: Failed to acquire lock for %s" ), aProjectPath );
1322 return false;
1323 }
1324
1325 return tagSaveAtHead( lock.GetRepository(), aFileType );
1326}
1327
1328bool LOCAL_HISTORY::HeadNewerThanLastSave( const wxString& aProjectPath )
1329{
1330 wxString hist = historyPath( aProjectPath );
1331 git_repository* repo = nullptr;
1332
1333 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1334 return false;
1335
1336 git_oid head_oid;
1337 if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
1338 {
1339 git_repository_free( repo );
1340 return false;
1341 }
1342
1343 git_commit* head_commit = nullptr;
1344 git_commit_lookup( &head_commit, repo, &head_oid );
1345 git_time_t head_time = git_commit_time( head_commit );
1346
1347 git_strarray tags;
1348 git_tag_list_match( &tags, "Last_Save_*", repo );
1349 git_time_t save_time = 0;
1350
1351 for( size_t i = 0; i < tags.count; ++i )
1352 {
1353 git_reference* ref = nullptr;
1354 if( git_reference_lookup( &ref, repo,
1355 ( wxS( "refs/tags/" ) +
1356 wxString::FromUTF8( tags.strings[i] ) ).mb_str().data() ) == 0 )
1357 {
1358 const git_oid* oid = git_reference_target( ref );
1359 git_commit* c = nullptr;
1360 if( git_commit_lookup( &c, repo, oid ) == 0 )
1361 {
1362 git_time_t t = git_commit_time( c );
1363 if( t > save_time )
1364 save_time = t;
1365 git_commit_free( c );
1366 }
1367 git_reference_free( ref );
1368 }
1369 }
1370
1371 git_strarray_free( &tags );
1372 git_commit_free( head_commit );
1373 git_repository_free( repo );
1374
1375 // If there are no Last_Save tags but there IS a HEAD commit, we have autosaved
1376 // data that was never explicitly saved - offer to restore
1377 if( save_time == 0 )
1378 return true;
1379
1380 return head_time > save_time;
1381}
1382
1383bool LOCAL_HISTORY::CommitDuplicateOfLastSave( const wxString& aProjectPath, const wxString& aFileType,
1384 const wxString& aMessage )
1385{
1386 if( !Pgm().GetCommonSettings()->m_Backup.enabled || !formatUsesIncrementalHistory() )
1387 return true;
1388
1389 if( !isProjectDirectory( aProjectPath ) )
1390 return false;
1391
1392 HISTORY_LOCK_MANAGER lock( aProjectPath );
1393
1394 if( !lock.IsLocked() )
1395 {
1396 wxLogTrace( traceAutoSave, wxS( "[history] CommitDuplicateOfLastSave: Failed to acquire lock for %s" ), aProjectPath );
1397 return false;
1398 }
1399
1400 git_repository* repo = lock.GetRepository();
1401
1402 if( !repo )
1403 return false;
1404
1405 wxString lastName; lastName.Printf( wxS("Last_Save_%s"), aFileType );
1406 git_reference* lastRef = nullptr;
1407 if( git_reference_lookup( &lastRef, repo, ( wxS("refs/tags/") + lastName ).mb_str().data() ) != 0 )
1408 return false; // no tag to duplicate
1409 std::unique_ptr<git_reference, decltype( &git_reference_free )> lastRefPtr( lastRef, &git_reference_free );
1410
1411 const git_oid* lastOid = git_reference_target( lastRef );
1412 git_commit* lastCommit = nullptr;
1413 if( git_commit_lookup( &lastCommit, repo, lastOid ) != 0 )
1414 return false;
1415 std::unique_ptr<git_commit, decltype( &git_commit_free )> lastCommitPtr( lastCommit, &git_commit_free );
1416
1417 git_tree* lastTree = nullptr;
1418 git_commit_tree( &lastTree, lastCommit );
1419 std::unique_ptr<git_tree, decltype( &git_tree_free )> lastTreePtr( lastTree, &git_tree_free );
1420
1421 // Parent will be current HEAD (to keep linear history)
1422 git_oid headOid;
1423 git_commit* headCommit = nullptr;
1424 int parents = 0;
1425 const git_commit* parentArray[1];
1426 if( git_reference_name_to_id( &headOid, repo, "HEAD" ) == 0 &&
1427 git_commit_lookup( &headCommit, repo, &headOid ) == 0 )
1428 {
1429 parentArray[0] = headCommit;
1430 parents = 1;
1431 }
1432
1433 git_signature* sigRaw = nullptr;
1434 git_signature_now( &sigRaw, "KiCad", "[email protected]" );
1435 std::unique_ptr<git_signature, decltype( &git_signature_free )> sig( sigRaw, &git_signature_free );
1436
1437 wxString msg = aMessage.IsEmpty() ? wxS("Discard unsaved ") + aFileType : aMessage;
1438 git_oid newCommitOid;
1439 int rc = git_commit_create( &newCommitOid, repo, "HEAD", sig.get(), sig.get(), nullptr,
1440 msg.mb_str().data(), lastTree, parents, parents ? parentArray : nullptr );
1441 if( headCommit ) git_commit_free( headCommit );
1442 if( rc != 0 )
1443 return false;
1444
1445 // Move Last_Save tag to new commit
1446 git_reference* existing = nullptr;
1447 if( git_reference_lookup( &existing, repo, ( wxS("refs/tags/") + lastName ).mb_str().data() ) == 0 )
1448 {
1449 git_reference_delete( existing );
1450 git_reference_free( existing );
1451 }
1452 git_object* newCommitObj = nullptr;
1453 if( git_object_lookup( &newCommitObj, repo, &newCommitOid, GIT_OBJECT_COMMIT ) == 0 )
1454 {
1455 git_tag_create_lightweight( &newCommitOid, repo, lastName.mb_str().data(), newCommitObj, 0 );
1456 git_object_free( newCommitObj );
1457 }
1458 return true;
1459}
1460
1461static size_t dirSizeRecursive( const wxString& path )
1462{
1463 size_t total = 0;
1464 wxDir dir( path );
1465 if( !dir.IsOpened() )
1466 return 0;
1467 wxString name;
1468 bool cont = dir.GetFirst( &name );
1469 while( cont )
1470 {
1471 wxFileName fn( path, name );
1472 wxString fullPath = fn.GetFullPath();
1473
1474 if( wxFileName::DirExists( fullPath ) )
1475 total += dirSizeRecursive( fullPath );
1476 else if( fn.FileExists() )
1477 total += (size_t) fn.GetSize().GetValue();
1478 cont = dir.GetNext( &name );
1479 }
1480 return total;
1481}
1482
1483// Copy tree and all blob objects directly between ODBs
1484static bool copyTreeObjects( git_repository* aSrcRepo, git_odb* aSrcOdb, git_odb* aDstOdb, const git_oid* aTreeOid,
1485 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )>& aCopied )
1486{
1487 if( aCopied.count( *aTreeOid ) )
1488 return true;
1489
1490 git_odb_object* obj = nullptr;
1491
1492 if( git_odb_read( &obj, aSrcOdb, aTreeOid ) != 0 )
1493 return false;
1494
1495 git_oid written;
1496 int err = git_odb_write( &written, aDstOdb, git_odb_object_data( obj ), git_odb_object_size( obj ),
1497 git_odb_object_type( obj ) );
1498 git_odb_object_free( obj );
1499
1500 if( err != 0 )
1501 return false;
1502
1503 aCopied.insert( *aTreeOid );
1504
1505 git_tree* tree = nullptr;
1506
1507 if( git_tree_lookup( &tree, aSrcRepo, aTreeOid ) != 0 )
1508 return false;
1509
1510 size_t cnt = git_tree_entrycount( tree );
1511
1512 for( size_t i = 0; i < cnt; ++i )
1513 {
1514 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1515 const git_oid* entryId = git_tree_entry_id( entry );
1516
1517 if( aCopied.count( *entryId ) )
1518 continue;
1519
1520 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1521 {
1522 if( !copyTreeObjects( aSrcRepo, aSrcOdb, aDstOdb, entryId, aCopied ) )
1523 {
1524 git_tree_free( tree );
1525 return false;
1526 }
1527 }
1528 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1529 {
1530 git_odb_object* blobObj = nullptr;
1531
1532 if( git_odb_read( &blobObj, aSrcOdb, entryId ) == 0 )
1533 {
1534 git_oid blobWritten;
1535
1536 if( git_odb_write( &blobWritten, aDstOdb, git_odb_object_data( blobObj ),
1537 git_odb_object_size( blobObj ), git_odb_object_type( blobObj ) )
1538 != 0 )
1539 {
1540 git_odb_object_free( blobObj );
1541 git_tree_free( tree );
1542 return false;
1543 }
1544
1545 git_odb_object_free( blobObj );
1546 aCopied.insert( *entryId );
1547 }
1548 }
1549 }
1550
1551 git_tree_free( tree );
1552 return true;
1553}
1554
1555
1556// Compact loose objects into a packfile and remove the originals.
1557// Equivalent to git gc
1558static bool compactRepository( git_repository* aRepo, PROGRESS_REPORTER* aReporter = nullptr )
1559{
1560 git_packbuilder* pb = nullptr;
1561
1562 if( git_packbuilder_new( &pb, aRepo ) != 0 )
1563 return false;
1564
1565 git_revwalk* walk = nullptr;
1566
1567 if( git_revwalk_new( &walk, aRepo ) != 0 )
1568 {
1569 git_packbuilder_free( pb );
1570 return false;
1571 }
1572
1573 git_revwalk_push_head( walk );
1574 git_oid oid;
1575
1576 while( git_revwalk_next( &oid, walk ) == 0 )
1577 {
1578 if( git_packbuilder_insert_commit( pb, &oid ) != 0 )
1579 {
1580 git_revwalk_free( walk );
1581 git_packbuilder_free( pb );
1582 return false;
1583 }
1584 }
1585
1586 git_revwalk_free( walk );
1587
1588 if( aReporter )
1589 {
1590 git_packbuilder_set_callbacks(
1591 pb,
1592 []( int aStage, uint32_t aCurrent, uint32_t aTotal, void* aPayload )
1593 {
1594 auto* reporter = static_cast<PROGRESS_REPORTER*>( aPayload );
1595
1596 if( aTotal > 0 )
1597 reporter->SetCurrentProgress( (double) aCurrent / aTotal );
1598
1599 reporter->KeepRefreshing();
1600 return 0;
1601 },
1602 aReporter );
1603 }
1604
1605 if( git_packbuilder_write( pb, nullptr, 0, nullptr, nullptr ) != 0 )
1606 {
1607 git_packbuilder_free( pb );
1608 return false;
1609 }
1610
1611 git_packbuilder_free( pb );
1612
1613 wxString objPath = wxString::FromUTF8( git_repository_path( aRepo ) ) + wxS( "objects" );
1614 wxDir objDir( objPath );
1615
1616 if( objDir.IsOpened() )
1617 {
1618 wxArrayString toRemove;
1619 wxString name;
1620 bool cont = objDir.GetFirst( &name, wxEmptyString, wxDIR_DIRS );
1621
1622 while( cont )
1623 {
1624 if( name.length() == 2 )
1625 toRemove.Add( objPath + wxFileName::GetPathSeparator() + name );
1626
1627 cont = objDir.GetNext( &name );
1628 }
1629
1630 for( const wxString& dir : toRemove )
1631 wxFileName::Rmdir( dir, wxPATH_RMDIR_RECURSIVE );
1632 }
1633
1634 return true;
1635}
1636
1637
1638bool LOCAL_HISTORY::EnforceSizeLimit( const wxString& aProjectPath, size_t aMaxBytes, PROGRESS_REPORTER* aReporter )
1639{
1640 if( aMaxBytes == 0 )
1641 return false;
1642
1643 wxString hist = historyPath( aProjectPath );
1644
1645 if( !wxDirExists( hist ) )
1646 return false;
1647
1648 size_t current = dirSizeRecursive( hist );
1649
1650 if( current <= aMaxBytes )
1651 return true; // within limit
1652
1653 HISTORY_LOCK_MANAGER lock( aProjectPath );
1654
1655 if( !lock.IsLocked() )
1656 {
1657 wxLogTrace( traceAutoSave, wxS( "[history] EnforceSizeLimit: Failed to acquire lock for %s" ), aProjectPath );
1658 return false;
1659 }
1660
1661 git_repository* repo = lock.GetRepository();
1662
1663 if( !repo )
1664 return false;
1665
1666 if( aReporter )
1667 aReporter->Report( _( "Compacting local history..." ) );
1668
1669 // Pack loose objects first. Can bring size within limit without a full rebuild.
1670 compactRepository( repo, aReporter );
1671
1672 current = dirSizeRecursive( hist );
1673
1674 if( current <= aMaxBytes )
1675 return true; // within limit after compaction
1676
1677 // Collect commits newest-first using revwalk
1678 git_revwalk* walk = nullptr;
1679 git_revwalk_new( &walk, repo );
1680 git_revwalk_sorting( walk, GIT_SORT_TIME );
1681 git_revwalk_push_head( walk );
1682 std::vector<git_oid> commits;
1683 git_oid oid;
1684
1685 while( git_revwalk_next( &oid, walk ) == 0 )
1686 commits.push_back( oid );
1687
1688 git_revwalk_free( walk );
1689
1690 if( commits.empty() )
1691 return true;
1692
1693 // Determine set of newest commits to keep based on blob sizes.
1694 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )> seenBlobs(
1695 []( const git_oid& a, const git_oid& b )
1696 {
1697 return memcmp( &a, &b, sizeof( git_oid ) ) < 0;
1698 } );
1699
1700 size_t keptBytes = 0;
1701 std::vector<git_oid> keep;
1702
1703 git_odb* odb = nullptr;
1704 git_repository_odb( &odb, repo );
1705
1706 std::function<size_t( git_tree* )> accountTree = [&]( git_tree* tree )
1707 {
1708 size_t added = 0;
1709 size_t cnt = git_tree_entrycount( tree );
1710
1711 for( size_t i = 0; i < cnt; ++i )
1712 {
1713 const git_tree_entry* entry = git_tree_entry_byindex( tree, i );
1714
1715 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
1716 {
1717 const git_oid* bid = git_tree_entry_id( entry );
1718
1719 if( seenBlobs.find( *bid ) == seenBlobs.end() )
1720 {
1721 size_t len = 0;
1722 git_object_t type = GIT_OBJECT_ANY;
1723
1724 if( odb && git_odb_read_header( &len, &type, odb, bid ) == 0 )
1725 added += len;
1726
1727 seenBlobs.insert( *bid );
1728 }
1729 }
1730 else if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
1731 {
1732 git_tree* sub = nullptr;
1733
1734 if( git_tree_lookup( &sub, repo, git_tree_entry_id( entry ) ) == 0 )
1735 {
1736 added += accountTree( sub );
1737 git_tree_free( sub );
1738 }
1739 }
1740 }
1741
1742 return added;
1743 };
1744
1745 for( const git_oid& cOid : commits )
1746 {
1747 git_commit* c = nullptr;
1748
1749 if( git_commit_lookup( &c, repo, &cOid ) != 0 )
1750 continue;
1751
1752 git_tree* tree = nullptr;
1753 git_commit_tree( &tree, c );
1754 size_t add = accountTree( tree );
1755 git_tree_free( tree );
1756 git_commit_free( c );
1757
1758 if( keep.empty() || keptBytes + add <= aMaxBytes )
1759 {
1760 keep.push_back( cOid );
1761 keptBytes += add;
1762 }
1763 else
1764 break; // stop once limit exceeded
1765 }
1766
1767 if( keep.empty() )
1768 keep.push_back( commits.front() );
1769
1770 // Collect tags we want to preserve (Save_*/Last_Save_*). We'll recreate them if their
1771 // target commit is retained. Also ensure tagged commits are ALWAYS kept.
1772 std::vector<std::pair<wxString, git_oid>> tagTargets;
1773 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )> taggedCommits(
1774 []( const git_oid& a, const git_oid& b )
1775 {
1776 return memcmp( &a, &b, sizeof( git_oid ) ) < 0;
1777 } );
1778 git_strarray tagList;
1779
1780 if( git_tag_list( &tagList, repo ) == 0 )
1781 {
1782 for( size_t i = 0; i < tagList.count; ++i )
1783 {
1784 wxString name = wxString::FromUTF8( tagList.strings[i] );
1785 if( name.StartsWith( wxS("Save_") ) || name.StartsWith( wxS("Last_Save_") ) )
1786 {
1787 git_reference* tref = nullptr;
1788
1789 if( git_reference_lookup( &tref, repo, ( wxS( "refs/tags/" ) + name ).mb_str().data() ) == 0 )
1790 {
1791 const git_oid* toid = git_reference_target( tref );
1792
1793 if( toid )
1794 {
1795 tagTargets.emplace_back( name, *toid );
1796 taggedCommits.insert( *toid );
1797
1798 // Ensure this tagged commit is in the keep list
1799 bool found = false;
1800 for( const auto& k : keep )
1801 {
1802 if( memcmp( &k, toid, sizeof( git_oid ) ) == 0 )
1803 {
1804 found = true;
1805 break;
1806 }
1807 }
1808
1809 if( !found )
1810 {
1811 // Add tagged commit to keep list (even if it exceeds size limit)
1812 keep.push_back( *toid );
1813 wxLogTrace( traceAutoSave, wxS( "[history] EnforceSizeLimit: Preserving tagged commit %s" ),
1814 name );
1815 }
1816 }
1817
1818 git_reference_free( tref );
1819 }
1820 }
1821 }
1822 git_strarray_free( &tagList );
1823 }
1824
1825 // Rebuild trimmed repo in temp dir
1826 wxFileName trimFn( hist + wxS("_trim"), wxEmptyString );
1827 wxString trimPath = trimFn.GetPath();
1828
1829 if( wxDirExists( trimPath ) )
1830 wxFileName::Rmdir( trimPath, wxPATH_RMDIR_RECURSIVE );
1831
1832 wxMkdir( trimPath );
1833 git_repository* newRepo = nullptr;
1834
1835 if( git_repository_init( &newRepo, trimPath.mb_str().data(), 0 ) != 0 )
1836 {
1837 git_odb_free( odb );
1838 return false;
1839 }
1840
1841 git_odb* dstOdb = nullptr;
1842
1843 if( git_repository_odb( &dstOdb, newRepo ) != 0 )
1844 {
1845 git_repository_free( newRepo );
1846 git_odb_free( odb );
1847 return false;
1848 }
1849
1850 std::set<git_oid, bool ( * )( const git_oid&, const git_oid& )> copiedObjects(
1851 []( const git_oid& a, const git_oid& b )
1852 {
1853 return memcmp( &a, &b, sizeof( git_oid ) ) < 0;
1854 } );
1855
1856 // Replay kept commits chronologically (oldest first) to preserve order.
1857 std::reverse( keep.begin(), keep.end() );
1858 git_commit* parent = nullptr;
1859 struct MAP_ENTRY { git_oid orig; git_oid neu; };
1860 std::vector<MAP_ENTRY> commitMap;
1861
1862 if( aReporter )
1863 {
1864 aReporter->AdvancePhase( _( "Trimming local history..." ) );
1865 aReporter->SetCurrentProgress( 0 );
1866 }
1867
1868 for( size_t idx = 0; idx < keep.size(); ++idx )
1869 {
1870 if( aReporter )
1871 aReporter->SetCurrentProgress( (double) idx / keep.size() );
1872
1873 const git_oid& co = keep[idx];
1874 git_commit* orig = nullptr;
1875
1876 if( git_commit_lookup( &orig, repo, &co ) != 0 )
1877 continue;
1878
1879 git_tree* tree = nullptr;
1880 git_commit_tree( &tree, orig );
1881
1882 copyTreeObjects( repo, odb, dstOdb, git_tree_id( tree ), copiedObjects );
1883
1884 git_tree* newTree = nullptr;
1885 git_tree_lookup( &newTree, newRepo, git_tree_id( tree ) );
1886
1887 git_tree_free( tree );
1888
1889 // Recreate original author/committer signatures preserving timestamp.
1890 const git_signature* origAuthor = git_commit_author( orig );
1891 const git_signature* origCommitter = git_commit_committer( orig );
1892 git_signature* sigAuthor = nullptr;
1893 git_signature* sigCommitter = nullptr;
1894
1895 git_signature_new( &sigAuthor, origAuthor->name, origAuthor->email,
1896 origAuthor->when.time, origAuthor->when.offset );
1897 git_signature_new( &sigCommitter, origCommitter->name, origCommitter->email,
1898 origCommitter->when.time, origCommitter->when.offset );
1899
1900 const git_commit* parents[1];
1901 int parentCount = 0;
1902
1903 if( parent )
1904 {
1905 parents[0] = parent;
1906 parentCount = 1;
1907 }
1908
1909 git_oid newCommitOid;
1910 git_commit_create( &newCommitOid, newRepo, "HEAD", sigAuthor, sigCommitter, nullptr, git_commit_message( orig ),
1911 newTree, parentCount, parentCount ? parents : nullptr );
1912
1913 if( parent )
1914 git_commit_free( parent );
1915
1916 git_commit_lookup( &parent, newRepo, &newCommitOid );
1917
1918 commitMap.emplace_back( co, newCommitOid );
1919
1920 git_signature_free( sigAuthor );
1921 git_signature_free( sigCommitter );
1922 git_tree_free( newTree );
1923 git_commit_free( orig );
1924 }
1925
1926 if( parent )
1927 git_commit_free( parent );
1928
1929 // Recreate preserved tags pointing to new commit OIDs where possible.
1930 for( const auto& tt : tagTargets )
1931 {
1932 // Find mapping
1933 const git_oid* newOid = nullptr;
1934
1935 for( const auto& m : commitMap )
1936 {
1937 if( memcmp( &m.orig, &tt.second, sizeof( git_oid ) ) == 0 )
1938 {
1939 newOid = &m.neu;
1940 break;
1941 }
1942 }
1943
1944 if( !newOid )
1945 continue; // commit trimmed away
1946
1947 git_object* obj = nullptr;
1948
1949 if( git_object_lookup( &obj, newRepo, newOid, GIT_OBJECT_COMMIT ) == 0 )
1950 {
1951 git_oid tag_oid; git_tag_create_lightweight( &tag_oid, newRepo, tt.first.mb_str().data(), obj, 0 );
1952 git_object_free( obj );
1953 }
1954 }
1955
1956 if( aReporter )
1957 aReporter->AdvancePhase( _( "Compacting trimmed history..." ) );
1958
1959 compactRepository( newRepo, aReporter );
1960
1961 // Free ODBs and close repos before swapping directories to avoid file locking issues.
1962 // Note: The lock manager will automatically free the original repo when it goes out of scope,
1963 // but we need to manually free the ODBs and new trimmed repo we created.
1964 git_odb_free( dstOdb );
1965 git_odb_free( odb );
1966 git_repository_free( newRepo );
1967
1968 lock.ReleaseRepository();
1969
1970 // Replace old history dir with trimmed one
1971 wxString backupOld = hist + wxS("_old");
1972 wxRenameFile( hist, backupOld );
1973 wxRenameFile( trimPath, hist );
1974 wxFileName::Rmdir( backupOld, wxPATH_RMDIR_RECURSIVE );
1975 return true;
1976}
1977
1978wxString LOCAL_HISTORY::GetHeadHash( const wxString& aProjectPath )
1979{
1980 wxString hist = historyPath( aProjectPath );
1981 git_repository* repo = nullptr;
1982
1983 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
1984 return wxEmptyString;
1985
1986 git_oid head_oid;
1987 if( git_reference_name_to_id( &head_oid, repo, "HEAD" ) != 0 )
1988 {
1989 git_repository_free( repo );
1990 return wxEmptyString;
1991 }
1992
1993 wxString hash = wxString::FromUTF8( git_oid_tostr_s( &head_oid ) );
1994 git_repository_free( repo );
1995 return hash;
1996}
1997
1998
1999// Helper functions for RestoreCommit
2000namespace
2001{
2002
2006bool checkForLockedFiles( const wxString& aProjectPath, std::vector<wxString>& aLockedFiles )
2007{
2008 std::function<void( const wxString& )> findLocks = [&]( const wxString& dirPath )
2009 {
2010 wxDir dir( dirPath );
2011 if( !dir.IsOpened() )
2012 return;
2013
2014 wxString filename;
2015 bool cont = dir.GetFirst( &filename );
2016
2017 while( cont )
2018 {
2019 wxFileName fullPath( dirPath, filename );
2020
2021 // Skip special directories
2022 if( filename == wxS(".history") || filename == wxS(".git") )
2023 {
2024 cont = dir.GetNext( &filename );
2025 continue;
2026 }
2027
2028 if( fullPath.DirExists() )
2029 {
2030 findLocks( fullPath.GetFullPath() );
2031 }
2032 else if( fullPath.FileExists()
2033 && filename.StartsWith( FILEEXT::LockFilePrefix )
2034 && filename.EndsWith( wxString( wxS( "." ) ) + FILEEXT::LockFileExtension ) )
2035 {
2036 // Reconstruct the original filename from the lock file name
2037 // Lock files are: ~<original>.<ext>.lck -> need to get <original>.<ext>
2038 wxString baseName = filename.Mid( FILEEXT::LockFilePrefix.length() );
2039 baseName = baseName.BeforeLast( '.' ); // Remove .lck
2040 wxFileName originalFile( dirPath, baseName );
2041
2042 // Check if this is a valid LOCKFILE (not stale and not ours)
2043 LOCKFILE testLock( originalFile.GetFullPath() );
2044 if( testLock.Valid() && !testLock.IsLockedByMe() )
2045 {
2046 aLockedFiles.push_back( fullPath.GetFullPath() );
2047 }
2048 }
2049
2050 cont = dir.GetNext( &filename );
2051 }
2052 };
2053
2054 findLocks( aProjectPath );
2055 return aLockedFiles.empty();
2056}
2057
2058
2062bool extractCommitToTemp( git_repository* aRepo, git_tree* aTree, const wxString& aTempPath )
2063{
2064 bool extractSuccess = true;
2065
2066 std::function<void( git_tree*, const wxString& )> extractTree =
2067 [&]( git_tree* t, const wxString& prefix )
2068 {
2069 if( !extractSuccess )
2070 return;
2071
2072 size_t cnt = git_tree_entrycount( t );
2073 for( size_t i = 0; i < cnt; ++i )
2074 {
2075 const git_tree_entry* entry = git_tree_entry_byindex( t, i );
2076 wxString name = wxString::FromUTF8( git_tree_entry_name( entry ) );
2077 wxString fullPath = prefix.IsEmpty() ? name : prefix + wxS("/") + name;
2078
2079 if( git_tree_entry_type( entry ) == GIT_OBJECT_TREE )
2080 {
2081 wxFileName dirPath( aTempPath + wxFileName::GetPathSeparator() + fullPath,
2082 wxEmptyString );
2083 if( !wxFileName::Mkdir( dirPath.GetPath(), 0777, wxPATH_MKDIR_FULL ) )
2084 {
2085 wxLogTrace( traceAutoSave,
2086 wxS( "[history] extractCommitToTemp: Failed to create directory '%s'" ),
2087 dirPath.GetPath() );
2088 extractSuccess = false;
2089 return;
2090 }
2091
2092 git_tree* sub = nullptr;
2093 if( git_tree_lookup( &sub, aRepo, git_tree_entry_id( entry ) ) == 0 )
2094 {
2095 extractTree( sub, fullPath );
2096 git_tree_free( sub );
2097 }
2098 }
2099 else if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB )
2100 {
2101 git_blob* blob = nullptr;
2102 if( git_blob_lookup( &blob, aRepo, git_tree_entry_id( entry ) ) == 0 )
2103 {
2104 wxFileName dst( aTempPath + wxFileName::GetPathSeparator() + fullPath );
2105
2106 wxFileName dstDir( dst );
2107 dstDir.SetFullName( wxEmptyString );
2108 wxFileName::Mkdir( dstDir.GetPath(), 0777, wxPATH_MKDIR_FULL );
2109
2110 wxFFile f( dst.GetFullPath(), wxT( "wb" ) );
2111 if( f.IsOpened() )
2112 {
2113 f.Write( git_blob_rawcontent( blob ), git_blob_rawsize( blob ) );
2114 f.Close();
2115 }
2116 else
2117 {
2118 wxLogTrace( traceAutoSave,
2119 wxS( "[history] extractCommitToTemp: Failed to write '%s'" ),
2120 dst.GetFullPath() );
2121 extractSuccess = false;
2122 git_blob_free( blob );
2123 return;
2124 }
2125
2126 git_blob_free( blob );
2127 }
2128 }
2129 }
2130 };
2131
2132 extractTree( aTree, wxEmptyString );
2133 return extractSuccess;
2134}
2135
2136
2143void collectRelativeFiles( const wxString& aRoot, const wxString& aDir, std::vector<wxString>& aOut )
2144{
2145 wxDir dir( aDir );
2146
2147 if( !dir.IsOpened() )
2148 return;
2149
2150 wxString name;
2151
2152 for( bool cont = dir.GetFirst( &name ); cont; cont = dir.GetNext( &name ) )
2153 {
2154 wxString full = aDir + wxFILE_SEP_PATH + name;
2155
2156 if( wxDirExists( full ) )
2157 {
2158 collectRelativeFiles( aRoot, full, aOut );
2159 }
2160 else if( wxFileExists( full ) )
2161 {
2162 wxString rel = full.Mid( aRoot.length() + 1 );
2163 rel.Replace( wxS( "\\" ), wxS( "/" ) );
2164 aOut.push_back( rel );
2165 }
2166 }
2167}
2168
2169
2178bool overlaySnapshotFiles( const wxString& aTempRestorePath, const wxString& aProjectPath, const wxString& aBackupPath )
2179{
2180 std::vector<wxString> relPaths;
2181 collectRelativeFiles( aTempRestorePath, aTempRestorePath, relPaths );
2182
2183 for( const wxString& rel : relPaths )
2184 {
2185 wxFileName src( aTempRestorePath + wxFILE_SEP_PATH + rel );
2186 wxFileName dst( aProjectPath + wxFILE_SEP_PATH + rel );
2187
2188 if( dst.FileExists() )
2189 {
2190 wxFileName bak( aBackupPath + wxFILE_SEP_PATH + rel );
2191
2192 if( !wxFileName::Mkdir( bak.GetPath(), 0777, wxPATH_MKDIR_FULL )
2193 || !wxCopyFile( dst.GetFullPath(), bak.GetFullPath(), true ) )
2194 {
2195 return false;
2196 }
2197 }
2198
2199 if( !wxFileName::Mkdir( dst.GetPath(), 0777, wxPATH_MKDIR_FULL )
2200 || !wxCopyFile( src.GetFullPath(), dst.GetFullPath(), true ) )
2201 {
2202 return false;
2203 }
2204 }
2205
2206 return true;
2207}
2208
2209
2210} // namespace
2211
2212
2213bool LOCAL_HISTORY::RestoreCommit( const wxString& aProjectPath, const wxString& aHash, wxWindow* aParent,
2214 bool aConfirm )
2215{
2216 // STEP 1: Verify no files are open by checking for LOCKFILEs
2217 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Checking for open files in %s" ),
2218 aProjectPath );
2219
2220 std::vector<wxString> lockedFiles;
2221 if( !checkForLockedFiles( aProjectPath, lockedFiles ) )
2222 {
2223 wxString lockList;
2224 for( const auto& f : lockedFiles )
2225 lockList += wxS("\n - ") + f;
2226
2227 wxLogTrace( traceAutoSave,
2228 wxS( "[history] RestoreCommit: Cannot restore - files are open:%s" ),
2229 lockList );
2230
2231 // Show user-visible warning dialog
2232 if( aParent )
2233 {
2234 wxString msg = _( "Cannot restore - the following files are open by another user:" );
2235 msg += lockList;
2236 wxMessageBox( msg, _( "Restore Failed" ), wxOK | wxICON_WARNING, aParent );
2237 }
2238 return false;
2239 }
2240
2241 // STEP 2: Acquire history lock and verify target commit
2242 HISTORY_LOCK_MANAGER lock( aProjectPath );
2243
2244 if( !lock.IsLocked() )
2245 {
2246 wxLogTrace( traceAutoSave,
2247 wxS( "[history] RestoreCommit: Failed to acquire lock for %s" ),
2248 aProjectPath );
2249 return false;
2250 }
2251
2252 git_repository* repo = lock.GetRepository();
2253 if( !repo )
2254 return false;
2255
2256 // Verify the target commit exists
2257 git_oid oid;
2258 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 )
2259 {
2260 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Invalid hash %s" ), aHash );
2261 return false;
2262 }
2263
2264 git_commit* commit = nullptr;
2265 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2266 {
2267 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Commit not found %s" ), aHash );
2268 return false;
2269 }
2270
2271 git_tree* tree = nullptr;
2272 git_commit_tree( &tree, commit );
2273
2274 // Confirm before overwriting working files. The recovery prompt already asked, so it passes
2275 // aConfirm = false. Nothing is changed yet, so cancel just returns.
2276 if( aConfirm && aParent )
2277 {
2278 wxDateTime when( (time_t) git_commit_time( commit ) );
2279
2280 KICAD_MESSAGE_DIALOG dlg( aParent,
2281 wxString::Format( _( "Restore the project to the version from %s?" ),
2282 when.Format( wxS( "%Y-%m-%d %H:%M:%S" ) ) ),
2283 _( "Restore Version" ), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION );
2284
2285 dlg.SetYesNoLabels( _( "Restore" ), _( "Cancel" ) );
2286 dlg.SetExtendedMessage( _( "Your current files are backed up first so you can undo the "
2287 "restore. Files that are not part of this version are left "
2288 "untouched." ) );
2289
2290 if( dlg.ShowModal() != wxID_YES )
2291 {
2292 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: User cancelled at confirm" ) );
2293 git_tree_free( tree );
2294 git_commit_free( commit );
2295 return false;
2296 }
2297 }
2298
2299 // Create pre-restore backup snapshot using the existing lock
2300 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Creating pre-restore backup" ) );
2301
2302 std::vector<wxString> backupFiles;
2303 collectProjectFiles( aProjectPath, backupFiles );
2304
2305 if( !backupFiles.empty() )
2306 {
2307 wxString hist = historyPath( aProjectPath );
2308 SNAPSHOT_COMMIT_RESULT backupResult = commitSnapshotWithLock( repo, lock.GetIndex(), hist, aProjectPath,
2309 backupFiles, wxS( "Pre-restore backup" ) );
2310
2311 if( backupResult == SNAPSHOT_COMMIT_RESULT::Error )
2312 {
2313 wxLogTrace( traceAutoSave,
2314 wxS( "[history] RestoreCommit: Failed to create pre-restore backup" ) );
2315 git_tree_free( tree );
2316 git_commit_free( commit );
2317 return false;
2318 }
2319
2320 if( backupResult == SNAPSHOT_COMMIT_RESULT::NoChanges )
2321 {
2322 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Current state already matches HEAD; "
2323 "continuing without a new backup commit" ) );
2324 }
2325 }
2326
2327 // STEP 3: Extract commit to temporary location
2328 wxString tempRestorePath = aProjectPath + wxS("_restore_temp");
2329
2330 if( wxDirExists( tempRestorePath ) )
2331 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2332
2333 if( !wxFileName::Mkdir( tempRestorePath, 0777, wxPATH_MKDIR_FULL ) )
2334 {
2335 wxLogTrace( traceAutoSave,
2336 wxS( "[history] RestoreCommit: Failed to create temp directory %s" ),
2337 tempRestorePath );
2338 git_tree_free( tree );
2339 git_commit_free( commit );
2340 return false;
2341 }
2342
2343 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Extracting to temp location %s" ),
2344 tempRestorePath );
2345
2346 if( !extractCommitToTemp( repo, tree, tempRestorePath ) )
2347 {
2348 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Extraction failed, cleaning up" ) );
2349 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2350 git_tree_free( tree );
2351 git_commit_free( commit );
2352 return false;
2353 }
2354
2355 // STEP 4: Overlay the snapshot onto the working copy. Restore never removes files that are
2356 // absent from the snapshot, so restoring a partial per-editor commit (for example the HEAD
2357 // autosave from a board-only session) cannot delete the schematic, project file, outputs, or
2358 // libraries. Overwritten files are archived to backupPath for manual recovery, and the
2359 // pre-restore commit created above is the full undo point.
2360 wxString backupPath =
2361 aProjectPath + wxS( "_restore_backup_" )
2362 + wxDateTime::UNow().Format( wxS( "%Y-%m-%dT%H-%M-%S-%l" ) );
2363
2364 if( !overlaySnapshotFiles( tempRestorePath, aProjectPath, backupPath ) )
2365 {
2366 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Overlay failed, rolling back from backup" ) );
2367
2368 // Put back whatever we already overwrote, then drop the partial backups.
2369 if( wxDirExists( backupPath ) )
2370 {
2371 wxString discard = aProjectPath + wxS( "_restore_discard" );
2372 overlaySnapshotFiles( backupPath, aProjectPath, discard );
2373 wxFileName::Rmdir( discard, wxPATH_RMDIR_RECURSIVE );
2374 wxFileName::Rmdir( backupPath, wxPATH_RMDIR_RECURSIVE );
2375 }
2376
2377 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2378 git_tree_free( tree );
2379 git_commit_free( commit );
2380 return false;
2381 }
2382
2383 // The backup directory is retained so the user can recover any displaced file.
2384 wxLogTrace( traceAutoSave,
2385 wxS( "[history] RestoreCommit: Restore successful, backup retained at %s" ),
2386 backupPath );
2387 wxFileName::Rmdir( tempRestorePath, wxPATH_RMDIR_RECURSIVE );
2388
2389 // Commit the full post-overlay project so HEAD and the saved baseline match the disk.
2390 std::vector<wxString> resultFiles;
2391 collectProjectFiles( aProjectPath, resultFiles );
2392 commitSnapshotWithLock( repo, lock.GetIndex(), historyPath( aProjectPath ), aProjectPath, resultFiles,
2393 wxString::Format( wxS( "Restored from %s" ), aHash ) );
2394
2395 // Anchor the saved baseline so reopening does not re-prompt.
2396 tagSaveAtHead( repo, wxS( "project" ) );
2397
2398 git_tree_free( tree );
2399 git_commit_free( commit );
2400
2401 wxLogTrace( traceAutoSave, wxS( "[history] RestoreCommit: Complete" ) );
2402 return true;
2403}
2404
2405void LOCAL_HISTORY::ShowRestoreDialog( const wxString& aProjectPath, wxWindow* aParent )
2406{
2407 if( !HistoryExists( aProjectPath ) )
2408 return;
2409
2410 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots = LoadSnapshots( aProjectPath );
2411
2412 if( snapshots.empty() )
2413 return;
2414
2415 DIALOG_RESTORE_LOCAL_HISTORY dlg( aParent, snapshots );
2416
2417 if( dlg.ShowModal() == wxID_OK )
2418 {
2419 wxString selectedHash = dlg.GetSelectedHash();
2420
2421 if( !selectedHash.IsEmpty() )
2422 RestoreCommit( aProjectPath, selectedHash, aParent );
2423 }
2424}
2425
2426std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> LOCAL_HISTORY::LoadSnapshots( const wxString& aProjectPath )
2427{
2428 std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> snapshots;
2429
2430 wxString hist = historyPath( aProjectPath );
2431 git_repository* repo = nullptr;
2432
2433 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2434 return snapshots;
2435
2436 git_revwalk* walk = nullptr;
2437 if( git_revwalk_new( &walk, repo ) != 0 )
2438 {
2439 git_repository_free( repo );
2440 return snapshots;
2441 }
2442
2443 git_revwalk_sorting( walk, GIT_SORT_TIME );
2444 git_revwalk_push_head( walk );
2445
2446 git_oid oid;
2447
2448 while( git_revwalk_next( &oid, walk ) == 0 )
2449 {
2450 git_commit* commit = nullptr;
2451
2452 if( git_commit_lookup( &commit, repo, &oid ) != 0 )
2453 continue;
2454
2456 info.hash = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
2457 info.date = wxDateTime( static_cast<time_t>( git_commit_time( commit ) ) );
2458 info.message = wxString::FromUTF8( git_commit_message( commit ) );
2459
2460 wxString firstLine = info.message.BeforeFirst( '\n' );
2461
2462 long parsedCount = 0;
2463 wxString remainder;
2464 firstLine.BeforeFirst( ':', &remainder );
2465 remainder.Trim( true ).Trim( false );
2466
2467 if( remainder.EndsWith( wxS( "files changed" ) ) )
2468 {
2469 wxString countText = remainder.BeforeFirst( ' ' );
2470
2471 if( countText.ToLong( &parsedCount ) )
2472 info.filesChanged = static_cast<int>( parsedCount );
2473 }
2474
2475 info.summary = firstLine.BeforeFirst( ':' );
2476
2477 wxString rest;
2478 info.message.BeforeFirst( '\n', &rest );
2479 wxArrayString lines = wxSplit( rest, '\n', '\0' );
2480
2481 for( const wxString& line : lines )
2482 {
2483 if( !line.IsEmpty() )
2484 info.changedFiles.Add( line );
2485 }
2486
2487 snapshots.push_back( std::move( info ) );
2488 git_commit_free( commit );
2489 }
2490
2491 git_revwalk_free( walk );
2492 git_repository_free( repo );
2493 return snapshots;
2494}
2495
2496
2497std::vector<LOCAL_HISTORY_SNAPSHOT_INFO> LOCAL_HISTORY::GetSnapshots( const wxString& aProjectPath )
2498{
2499 return LoadSnapshots( aProjectPath );
2500}
2501
2502
2503wxString LOCAL_HISTORY::TreeFingerprint( const wxString& aProjectPath, const wxString& aHash,
2504 const wxString& aExtension )
2505{
2506 wxString hist = historyPath( aProjectPath );
2507 git_repository* repo = nullptr;
2508
2509 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2510 return wxEmptyString;
2511
2512 git_oid oid;
2513 git_commit* commit = nullptr;
2514 git_tree* tree = nullptr;
2515
2516 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2517 {
2518 git_repository_free( repo );
2519 return wxEmptyString;
2520 }
2521
2522 if( git_commit_tree( &tree, commit ) != 0 )
2523 {
2524 git_commit_free( commit );
2525 git_repository_free( repo );
2526 return wxEmptyString;
2527 }
2528
2529 struct WALK_CTX
2530 {
2531 wxString ext;
2532 std::vector<wxString> entries;
2533 } ctx{ aExtension, {} };
2534
2535 auto collect = []( const char* aRoot, const git_tree_entry* aEntry, void* aPayload ) -> int
2536 {
2537 WALK_CTX* c = static_cast<WALK_CTX*>( aPayload );
2538
2539 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2540 return 0;
2541
2542 wxString name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2543
2544 if( !name.EndsWith( c->ext ) )
2545 return 0;
2546
2547 wxString path = wxString::FromUTF8( aRoot ) + name;
2548 c->entries.push_back( path + wxS( ":" )
2549 + wxString::FromUTF8( git_oid_tostr_s( git_tree_entry_id( aEntry ) ) ) );
2550 return 0;
2551 };
2552
2553 git_tree_walk( tree, GIT_TREEWALK_PRE, collect, &ctx );
2554
2555 git_tree_free( tree );
2556 git_commit_free( commit );
2557 git_repository_free( repo );
2558
2559 std::sort( ctx.entries.begin(), ctx.entries.end() );
2560
2561 wxString fingerprint;
2562
2563 for( const wxString& entry : ctx.entries )
2564 fingerprint << entry << wxS( "|" );
2565
2566 return fingerprint;
2567}
2568
2569
2570bool LOCAL_HISTORY::ExtractAllFilesAtCommit( const wxString& aProjectPath, const wxString& aHash,
2571 const wxString& aDestDir, const std::vector<wxString>& aExtensions )
2572{
2573 wxString hist = historyPath( aProjectPath );
2574 git_repository* repo = nullptr;
2575
2576 if( git_repository_open( &repo, hist.mb_str().data() ) != 0 )
2577 return false;
2578
2579 git_oid oid;
2580 git_commit* commit = nullptr;
2581 git_tree* tree = nullptr;
2582
2583 if( git_oid_fromstr( &oid, aHash.mb_str().data() ) != 0 || git_commit_lookup( &commit, repo, &oid ) != 0 )
2584 {
2585 git_repository_free( repo );
2586 return false;
2587 }
2588
2589 if( git_commit_tree( &tree, commit ) != 0 )
2590 {
2591 git_commit_free( commit );
2592 git_repository_free( repo );
2593 return false;
2594 }
2595
2596 struct WALK_CTX
2597 {
2598 git_repository* repo;
2599 wxString destDir;
2600 const std::vector<wxString>* extensions;
2601 bool ok;
2602 } ctx{ repo, aDestDir, &aExtensions, true };
2603
2604 // Non-capturing so it converts to the libgit2 C callback; state goes via payload.
2605 auto writeEntry = []( const char* aRoot, const git_tree_entry* aEntry, void* aPayload ) -> int
2606 {
2607 WALK_CTX* c = static_cast<WALK_CTX*>( aPayload );
2608
2609 if( git_tree_entry_type( aEntry ) != GIT_OBJECT_BLOB )
2610 return 0;
2611
2612 wxString name = wxString::FromUTF8( git_tree_entry_name( aEntry ) );
2613
2614 if( !c->extensions->empty() )
2615 {
2616 bool match = false;
2617
2618 for( const wxString& ext : *c->extensions )
2619 {
2620 if( name.EndsWith( ext ) )
2621 {
2622 match = true;
2623 break;
2624 }
2625 }
2626
2627 if( !match )
2628 return 0;
2629 }
2630
2631 wxString rel = wxString::FromUTF8( aRoot ) + name;
2632 wxFileName outFn( c->destDir + wxS( "/" ) + rel );
2633
2634 if( !wxFileName::Mkdir( outFn.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
2635 {
2636 c->ok = false;
2637 return 0;
2638 }
2639
2640 git_blob* blob = nullptr;
2641
2642 if( git_blob_lookup( &blob, c->repo, git_tree_entry_id( aEntry ) ) == 0 )
2643 {
2644 const void* data = git_blob_rawcontent( blob );
2645 const size_t size = static_cast<size_t>( git_blob_rawsize( blob ) );
2646 wxFFile out( outFn.GetFullPath(), wxS( "wb" ) );
2647
2648 if( !( data && out.IsOpened() && out.Write( data, size ) == size ) )
2649 c->ok = false;
2650
2651 git_blob_free( blob );
2652 }
2653 else
2654 {
2655 c->ok = false;
2656 }
2657
2658 return 0;
2659 };
2660
2661 git_tree_walk( tree, GIT_TREEWALK_PRE, writeEntry, &ctx );
2662
2663 git_tree_free( tree );
2664 git_commit_free( commit );
2665 git_repository_free( repo );
2666 return ctx.ok;
2667}
int index
const char * name
AUTO_BACKUP m_Backup
int ShowModal() override
Hybrid locking mechanism for local history git repositories.
git_repository * GetRepository()
Get the git repository handle (only valid if IsLocked() returns true).
void ReleaseRepository()
Release git repository and index handles early, but keep the file lock.
wxString GetLockError() const
Get error message describing why lock could not be acquired.
git_index * GetIndex()
Get the git index handle (only valid if IsLocked() returns true).
bool IsLocked() const
Check if locks were successfully acquired.
std::vector< LOCAL_HISTORY_SNAPSHOT_INFO > LoadSnapshots(const wxString &aProjectPath)
bool EnforceSizeLimit(const wxString &aProjectPath, size_t aMaxBytes, PROGRESS_REPORTER *aReporter=nullptr)
Enforce total size limit by rebuilding trimmed history keeping newest commits whose cumulative unique...
bool TagSave(const wxString &aProjectPath, const wxString &aFileType)
Tag a manual save in the local history repository.
bool RunRegisteredSaversAndCommit(const wxString &aProjectPath, const wxString &aTitle, const wxString &aTagFileType=wxEmptyString)
Run all registered savers and, if any staged changes differ from HEAD, create a commit.
std::vector< std::pair< wxString, wxString > > FindStaleAutosaveFiles(const wxString &aProjectPath, const std::vector< wxString > &aExtensions) const
Enumerate autosave files newer than their corresponding source files for the project at aProjectPath,...
wxString GetHeadHash(const wxString &aProjectPath)
Return the current head commit hash.
bool commitInBackground(const wxString &aProjectPath, const wxString &aTitle, const std::vector< HISTORY_FILE_DATA > &aFileData, bool aIsManualSave)
Execute file writes and git commit on a background thread.
void ShowRestoreDialog(const wxString &aProjectPath, wxWindow *aParent)
Show a dialog allowing the user to choose a snapshot to restore.
bool HeadNewerThanLastSave(const wxString &aProjectPath)
Return true if the autosave data is newer than the last manual save.
std::set< wxString > m_pendingFiles
std::map< const void *, std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> > m_savers
bool CommitDuplicateOfLastSave(const wxString &aProjectPath, const wxString &aFileType, const wxString &aMessage)
Create a new commit duplicating the tree pointed to by Last_Save_<fileType> and move the Last_Save_<f...
void WaitForPendingSave()
Block until any pending background save completes.
bool RestoreCommit(const wxString &aProjectPath, const wxString &aHash, wxWindow *aParent=nullptr, bool aConfirm=true)
Restore the project files to the state recorded by the given commit hash.
void RegisterSaver(const void *aSaverObject, const std::function< void(const wxString &, std::vector< HISTORY_FILE_DATA > &)> &aSaver)
Register a saver callback invoked during autosave history commits.
bool Init(const wxString &aProjectPath)
Initialize the local history repository for the given project path.
void ClearAllSavers()
Clear all registered savers.
bool CommitSnapshot(const std::vector< wxString > &aFiles, const wxString &aTitle)
Commit the given files to the local history repository.
std::atomic< bool > m_saveInProgress
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 RunRegisteredSaversAsAutosaveFiles(const wxString &aProjectPath)
Run all registered savers and write their output to autosave files instead of committing to the local...
bool CommitFullProjectSnapshot(const wxString &aProjectPath, const wxString &aTitle)
Commit a snapshot of the entire project directory (excluding the .history directory and ignored trans...
std::vector< LOCAL_HISTORY_SNAPSHOT_INFO > GetSnapshots(const wxString &aProjectPath)
Snapshots (commits) for the project, newest first.
wxString TreeFingerprint(const wxString &aProjectPath, const wxString &aHash, const wxString &aExtension)
Fingerprint of all files ending in aExtension recorded by commit aHash (sorted path:blob pairs).
bool ExtractAllFilesAtCommit(const wxString &aProjectPath, const wxString &aHash, const wxString &aDestDir, const std::vector< wxString > &aExtensions={})
Write files recorded at aHash into aDestDir, recreating the project's relative folder structure.
std::future< bool > m_pendingFuture
void UnregisterSaver(const void *aSaverObject)
Unregister a previously registered saver callback.
void RemoveAutosaveFiles(const wxString &aProjectPath) const
Remove every autosave file under the project at aProjectPath regardless of which source it shadowed.
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
Definition paths.cpp:518
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:553
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:124
A progress reporter interface for use in multi-threaded environments.
virtual void Report(const wxString &aMessage)=0
Display aMessage in the progress bar dialog.
virtual void AdvancePhase()=0
Use the next available virtual zone of the dialog progress bar.
virtual void SetCurrentProgress(double aProgress)=0
Set the progress value to aProgress (0..1).
COMMON_SETTINGS * GetCommonSettings() const
Retrieve the common settings shared by all applications.
wxString GetAutosaveRootForProject(const PROJECT *aProject=nullptr) const
Resolve the autosave-files root for a project.
PROJECT * GetProjectForPath(const wxString &aProjectPath) const
Return the active project iff its path matches aProjectPath, else nullptr.
wxString GetLocalHistoryDirForPath(const wxString &aProjectPath) const
Resolve the local-history directory for a project given by its on-disk path.
@ INCREMENTAL
Git-based local history (default)
BACKUP_LOCATION
@ PROJECT_DIR
Inside the project directory (default)
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
Definition confirm.h:48
#define _(s)
static const std::string LockFileExtension
static const std::string ProjectFileExtension
static const std::string LockFilePrefix
const wxChar *const traceAutoSave
Flag to enable auto save feature debug tracing.
static wxString historyPath(const wxString &aProjectPath)
static wxString historyPath(const wxString &aProjectPath)
static bool compactRepository(git_repository *aRepo, PROGRESS_REPORTER *aReporter=nullptr)
static bool isRestoreProtectedEntry(const wxString &aName)
static std::vector< std::pair< wxString, wxString > > findAutosaveFilePairs(const wxString &aProjectPath)
static const wxString AUTOSAVE_PREFIX
static bool commitSnapshotForProject(const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
static size_t dirSizeRecursive(const wxString &path)
static bool copyTreeObjects(git_repository *aSrcRepo, git_odb *aSrcOdb, git_odb *aDstOdb, const git_oid *aTreeOid, std::set< git_oid, bool(*)(const git_oid &, const git_oid &)> &aCopied)
static bool isKiCadProjectFile(const wxFileName &aFile)
static wxString sourceForAutosaveFile(const wxString &aAutosavePath, const wxString &aProjectPath, const wxString &aAutosaveRoot, BACKUP_LOCATION aLocation)
static bool tagSaveAtHead(git_repository *repo, const wxString &aFileType)
static wxString resolveAutosaveDestination(const wxString &aAutosaveRoot, const wxString &aRelativePath, BACKUP_LOCATION aLocation)
static SNAPSHOT_COMMIT_RESULT commitSnapshotWithLock(git_repository *repo, git_index *index, const wxString &aHistoryPath, const wxString &aProjectPath, const std::vector< wxString > &aFiles, const wxString &aTitle)
SNAPSHOT_COMMIT_RESULT
static bool filesContentEqual(const wxString &aPathA, const wxString &aPathB)
static bool formatUsesIncrementalHistory()
static bool isProjectDirectory(const wxString &aProjectPath)
static void collectProjectFiles(const wxString &aProjectPath, std::vector< wxString > &aFiles)
static wxString joinHistoryDestination(const wxString &aHistoryRoot, const wxString &aRelativePath)
File locking utilities.
void Prettify(std::string &aSource, FORMAT_MODE aMode)
Pretty-prints s-expression text according to KiCad format rules.
bool AtomicWriteFile(const wxString &aTargetPath, const void *aData, size_t aSize, wxString *aError=nullptr)
Writes aData to aTargetPath via a sibling temp file, fsyncs the data and directory,...
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
#define PROJECT_BACKUPS_DIR_SUFFIX
Project settings path will be <projectname> + this.
BACKUP_LOCATION location
Where backups, history, and autosave files live.
BACKUP_FORMAT format
Backup format (incremental git history vs zip archives)
Data produced by a registered saver on the UI thread, consumed by either the background local-history...
std::string path
IbisParser parser & reporter
VECTOR2I location
wxString result
Test unit parsing edge cases and error handling.
int delta
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
wxLogTrace helper definitions.
Definition of file extensions used in Kicad.