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