11using System ;
22using System . Collections . Generic ;
33using System . IO ;
4+ using System . Linq ;
45using System . Threading ;
56using System . Threading . Tasks ;
67
@@ -31,6 +32,12 @@ public bool EnableOFPADecoding
3132
3233 public IReadOnlyDictionary < string , string > DecodedPaths => _decodedPaths ;
3334
35+ public bool IsOFPASupported
36+ {
37+ get => _isOFPASupported ;
38+ private set => SetProperty ( ref _isOFPASupported , value ) ;
39+ }
40+
3441 public Repository Repository
3542 {
3643 get => _repo ;
@@ -248,6 +255,7 @@ public WorkingCopy(Repository repo)
248255
249256 public void Dispose ( )
250257 {
258+ _isDisposed = true ;
251259 _repo = null ;
252260 _inProgressContext = null ;
253261
@@ -269,6 +277,7 @@ public void Dispose()
269277 _staged . Clear ( ) ;
270278 OnPropertyChanged ( nameof ( Staged ) ) ;
271279
280+ ClearDecodedPaths ( ) ;
272281 _detailContext = null ;
273282 _commitMessage = string . Empty ;
274283 }
@@ -340,6 +349,8 @@ public void SetData(List<Models.Change> changes, CancellationToken cancellationT
340349
341350 _isLoadingData = true ;
342351 _cached = changes ;
352+ IsOFPASupported = Directory . EnumerateFiles ( _repo . FullPath , "*.uproject" ) . Any ( ) ||
353+ Directory . EnumerateFiles ( _repo . FullPath , "*.uplugin" ) . Any ( ) ;
343354 HasUnsolvedConflicts = hasConflict ;
344355 VisibleUnstaged = visibleUnstaged ;
345356 VisibleStaged = visibleStaged ;
@@ -858,41 +869,107 @@ private bool IsChanged(List<Models.Change> old, List<Models.Change> cur)
858869
859870 private bool _hasUnsolvedConflicts = false ;
860871 private InProgressContext _inProgressContext = null ;
872+ private bool _isDisposed = false ;
873+ private bool _isOFPASupported = false ;
861874 private Dictionary < string , string > _decodedPaths = new Dictionary < string , string > ( ) ;
875+ // Tracks file size/mtime for decoded paths to refresh when content changes.
876+ private Dictionary < string , ( long Length , DateTime LastWriteUtc ) > _decodedPathStats = new Dictionary < string , ( long Length , DateTime LastWriteUtc ) > ( ) ;
862877
863878 private async Task DecodeOFPAPathsAsync ( List < Models . Change > changes )
864879 {
865- if ( ! _repo . Settings . EnableOFPADecoding || changes == null || changes . Count == 0 )
880+ if ( _isDisposed || _repo == null || ! _repo . Settings . EnableOFPADecoding || changes == null || changes . Count == 0 )
866881 return ;
867882
868883 var repositoryPath = _repo . FullPath ;
869884 var filesToProcess = new List < ( string RelativePath , string FullPath ) > ( ) ;
885+ var missingWorkingTreePaths = new List < string > ( ) ;
870886
871887 foreach ( var change in changes )
872888 {
873- if ( Utilities . OFPAParser . IsOFPAFile ( change . Path ) &&
874- ! _decodedPaths . ContainsKey ( change . Path ) )
889+ if ( ! Utilities . OFPAParser . IsOFPAFile ( change . Path ) )
890+ continue ;
891+
892+ var fullPath = Path . Combine ( repositoryPath , change . Path ) ;
893+ if ( File . Exists ( fullPath ) )
894+ {
895+ try
896+ {
897+ var info = new FileInfo ( fullPath ) ;
898+ // Skip re-decoding if content stats are unchanged.
899+ if ( _decodedPaths . ContainsKey ( change . Path ) &&
900+ _decodedPathStats . TryGetValue ( change . Path , out var stats ) &&
901+ stats . Length == info . Length &&
902+ stats . LastWriteUtc == info . LastWriteTimeUtc )
903+ continue ;
904+ }
905+ catch
906+ {
907+ // File might disappear between Exists and stat access.
908+ missingWorkingTreePaths . Add ( change . Path ) ;
909+ }
910+ }
911+ else
875912 {
876- var fullPath = Path . Combine ( repositoryPath , change . Path ) ;
877- if ( File . Exists ( fullPath ) )
878- filesToProcess . Add ( ( change . Path , fullPath ) ) ;
913+ // Remove stats when the working-tree file disappears.
914+ if ( _decodedPathStats . ContainsKey ( change . Path ) )
915+ missingWorkingTreePaths . Add ( change . Path ) ;
916+
917+ if ( _decodedPaths . ContainsKey ( change . Path ) )
918+ continue ;
879919 }
920+
921+ filesToProcess . Add ( ( change . Path , fullPath ) ) ;
880922 }
881923
882- if ( filesToProcess . Count == 0 )
924+ if ( filesToProcess . Count == 0 && missingWorkingTreePaths . Count == 0 )
883925 return ;
884926
885927 var results = new Dictionary < string , string > ( ) ;
928+ var updatedStats = new Dictionary < string , ( long Length , DateTime LastWriteUtc ) > ( ) ;
929+
930+ // Read bytes from either MemoryStream or a generic stream.
931+ static async Task < byte [ ] > ReadAllBytesAsync ( Stream stream )
932+ {
933+ if ( stream is MemoryStream memoryStream )
934+ return memoryStream . ToArray ( ) ;
935+
936+ await using var buffer = new MemoryStream ( ) ;
937+ await stream . CopyToAsync ( buffer ) . ConfigureAwait ( false ) ;
938+ return buffer . ToArray ( ) ;
939+ }
886940
887- await Task . Run ( ( ) =>
941+ await Task . Run ( async ( ) =>
888942 {
889943 foreach ( var ( relativePath , fullPath ) in filesToProcess )
890944 {
891945 try
892946 {
893- var result = Utilities . OFPAParser . Decode ( fullPath ) ;
894- if ( result . HasValue )
895- results [ relativePath ] = result . Value . LabelValue ;
947+ if ( File . Exists ( fullPath ) )
948+ {
949+ // Prefer working-tree content when available.
950+ var result = Utilities . OFPAParser . Decode ( fullPath ) ;
951+ if ( result . HasValue )
952+ {
953+ results [ relativePath ] = result . Value . LabelValue ;
954+ var info = new FileInfo ( fullPath ) ;
955+ updatedStats [ relativePath ] = ( info . Length , info . LastWriteTimeUtc ) ;
956+ }
957+
958+ continue ;
959+ }
960+
961+ // Fallback to Git index/HEAD for missing working-tree files.
962+ await using var indexStream = await Commands . QueryFileContent . RunIndexAsync ( repositoryPath , relativePath ) . ConfigureAwait ( false ) ;
963+ var data = await ReadAllBytesAsync ( indexStream ) . ConfigureAwait ( false ) ;
964+ if ( data . Length == 0 )
965+ {
966+ await using var headStream = await Commands . QueryFileContent . RunAsync ( repositoryPath , "HEAD" , relativePath ) . ConfigureAwait ( false ) ;
967+ data = await ReadAllBytesAsync ( headStream ) . ConfigureAwait ( false ) ;
968+ }
969+
970+ var decoded = data . Length > 0 ? Utilities . OFPAParser . DecodeFromData ( data ) : null ;
971+ if ( decoded . HasValue )
972+ results [ relativePath ] = decoded . Value . LabelValue ;
896973 }
897974 catch
898975 {
@@ -901,18 +978,34 @@ await Task.Run(() =>
901978 }
902979 } ) ;
903980
904- if ( results . Count > 0 )
981+ if ( results . Count > 0 || missingWorkingTreePaths . Count > 0 || updatedStats . Count > 0 )
905982 {
906983 await Dispatcher . UIThread . InvokeAsync ( ( ) =>
907984 {
908- if ( ! _repo . Settings . EnableOFPADecoding )
985+ if ( _isDisposed || _repo == null || ! _repo . Settings . EnableOFPADecoding )
909986 return ;
910987
911- var updated = new Dictionary < string , string > ( _decodedPaths ) ;
912- foreach ( var kvp in results )
913- updated [ kvp . Key ] = kvp . Value ;
988+ if ( results . Count > 0 )
989+ {
990+ var updated = new Dictionary < string , string > ( _decodedPaths ) ;
991+ foreach ( var kvp in results )
992+ updated [ kvp . Key ] = kvp . Value ;
993+
994+ _decodedPaths = updated ;
995+ }
996+
997+ if ( missingWorkingTreePaths . Count > 0 )
998+ {
999+ foreach ( var path in missingWorkingTreePaths )
1000+ _decodedPathStats . Remove ( path ) ;
1001+ }
1002+
1003+ if ( updatedStats . Count > 0 )
1004+ {
1005+ foreach ( var kvp in updatedStats )
1006+ _decodedPathStats [ kvp . Key ] = kvp . Value ;
1007+ }
9141008
915- _decodedPaths = updated ;
9161009 OnPropertyChanged ( nameof ( DecodedPaths ) ) ;
9171010 } ) ;
9181011 }
@@ -921,6 +1014,7 @@ await Dispatcher.UIThread.InvokeAsync(() =>
9211014 private void ClearDecodedPaths ( )
9221015 {
9231016 _decodedPaths = new Dictionary < string , string > ( ) ;
1017+ _decodedPathStats = new Dictionary < string , ( long Length , DateTime LastWriteUtc ) > ( ) ;
9241018 OnPropertyChanged ( nameof ( DecodedPaths ) ) ;
9251019 }
9261020 }
0 commit comments