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