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