Skip to content

Commit 6ff8826

Browse files
NainetenNaineten
authored andcommitted
feat: extend OFPA decoding to Commit History and Stashes view
Applied the OFPA filename decoding logic to historical views (Commit Detail and Stashes). Changes: - Implemented async decoding in CommitDetail and StashesPage ViewModels. - Updated CommitChanges, CommitDetail (Info tab), and StashesPage Views to bind to DecodedPaths. - Reused efficient batch git processing (git cat-file) to read file content from specific revisions (SHA/Parent) without checking out files. - Guarded by the same EnableUnrealEngineSupport setting.
1 parent bda0c6c commit 6ff8826

5 files changed

Lines changed: 214 additions & 1 deletion

File tree

src/ViewModels/CommitDetail.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ public Repository Repository
3333
get => _repo;
3434
}
3535

36+
public IReadOnlyDictionary<string, string> DecodedPaths
37+
{
38+
get
39+
{
40+
if (_repo == null || !_repo.Settings.EnableUnrealEngineSupport || !_repo.Settings.EnableOFPADecoding)
41+
return null;
42+
return _decodedPaths;
43+
}
44+
}
45+
3646
public int ActiveTabIndex
3747
{
3848
get => _sharedData.ActiveTabIndex;
@@ -173,10 +183,16 @@ public CommitDetail(Repository repo, CommitDetailSharedData sharedData)
173183
_repo = repo;
174184
_sharedData = sharedData ?? new CommitDetailSharedData();
175185
WebLinks = Models.CommitLink.Get(repo.Remotes);
186+
187+
if (_repo != null)
188+
_repo.PropertyChanged += OnRepositoryPropertyChanged;
176189
}
177190

178191
public void Dispose()
179192
{
193+
if (_repo != null)
194+
_repo.PropertyChanged -= OnRepositoryPropertyChanged;
195+
180196
_repo = null;
181197
_commit = null;
182198
_changes = null;
@@ -190,6 +206,7 @@ public void Dispose()
190206
_requestingRevisionFiles = false;
191207
_revisionFiles = null;
192208
_revisionFileSearchSuggestion = null;
209+
_decodedPaths = null;
193210
}
194211

195212
public void NavigateTo(string commitSHA)
@@ -444,6 +461,9 @@ private void Refresh()
444461
SelectedChanges = null;
445462
else
446463
SelectedChanges = [VisibleChanges[0]];
464+
465+
if (_repo.Settings.EnableUnrealEngineSupport && _repo.Settings.EnableOFPADecoding)
466+
_currentDecodeTask = DecodeOFPAPathsAsync(changes);
447467
});
448468
}
449469
}, token);
@@ -647,6 +667,80 @@ private async Task SetViewingCommitAsync(Models.Object file)
647667
[GeneratedRegex(@"`.*?`")]
648668
private static partial Regex REG_INLINECODE_FORMAT();
649669

670+
private void OnRepositoryPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
671+
{
672+
if (e.PropertyName == nameof(Repository.EnableUnrealEngineSupport))
673+
{
674+
OnPropertyChanged(nameof(DecodedPaths));
675+
if (_repo.Settings.EnableUnrealEngineSupport && _repo.Settings.EnableOFPADecoding)
676+
_currentDecodeTask = DecodeOFPAPathsAsync(_changes);
677+
}
678+
}
679+
680+
private async Task DecodeOFPAPathsAsync(List<Models.Change> changes)
681+
{
682+
await _currentDecodeTask.ConfigureAwait(false);
683+
684+
if (_repo == null || _commit == null ||
685+
!_repo.Settings.EnableUnrealEngineSupport ||
686+
!_repo.Settings.EnableOFPADecoding ||
687+
changes == null || changes.Count == 0)
688+
return;
689+
690+
var repositoryPath = _repo.FullPath;
691+
var parent = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : $"{_commit.SHA}^";
692+
var results = new Dictionary<string, string>(StringComparer.Ordinal);
693+
694+
await Task.Run(async () =>
695+
{
696+
var objectSpecs = new List<(string RelativePath, string Spec)>();
697+
foreach (var change in changes)
698+
{
699+
if (Utilities.OFPAParser.IsOFPAFile(change.Path))
700+
{
701+
// For deleted files, read from parent. For others, read from current commit.
702+
var spec = (change.WorkTree == Models.ChangeState.Deleted || change.Index == Models.ChangeState.Deleted)
703+
? $"{parent}:{change.Path}"
704+
: $"{_commit.SHA}:{change.Path}";
705+
objectSpecs.Add((change.Path, spec));
706+
}
707+
}
708+
709+
if (objectSpecs.Count == 0)
710+
return;
711+
712+
var specs = new List<string>();
713+
foreach (var entry in objectSpecs)
714+
specs.Add(entry.Spec);
715+
716+
var dataMap = await Commands.QueryFileContent.RunBatchAsync(repositoryPath, specs, MaxOFPASampleSize).ConfigureAwait(false);
717+
foreach (var entry in objectSpecs)
718+
{
719+
if (dataMap.TryGetValue(entry.Spec, out var data))
720+
{
721+
var decoded = Utilities.OFPAParser.DecodeFromData(data);
722+
results[entry.RelativePath] = decoded?.LabelValue;
723+
}
724+
}
725+
726+
var updated = new Dictionary<string, string>(StringComparer.Ordinal);
727+
foreach (var kvp in results)
728+
updated[kvp.Key] = kvp.Value;
729+
_decodedPaths = updated;
730+
});
731+
732+
if (results.Count > 0)
733+
{
734+
await Dispatcher.UIThread.InvokeAsync(() =>
735+
{
736+
if (_repo == null || !_repo.Settings.EnableUnrealEngineSupport || !_repo.Settings.EnableOFPADecoding)
737+
return;
738+
739+
OnPropertyChanged(nameof(DecodedPaths));
740+
});
741+
}
742+
}
743+
650744
private Repository _repo = null;
651745
private CommitDetailSharedData _sharedData = null;
652746
private Models.Commit _commit = null;
@@ -667,5 +761,8 @@ private async Task SetViewingCommitAsync(Models.Object file)
667761
private List<string> _revisionFileSearchSuggestion = null;
668762
private bool _canOpenRevisionFileWithDefaultEditor = false;
669763
private Vector _scrollOffset = Vector.Zero;
764+
private Dictionary<string, string> _decodedPaths = null;
765+
private Task _currentDecodeTask = Task.CompletedTask;
766+
private const int MaxOFPASampleSize = 256 * 1024;
670767
}
671768
}

src/ViewModels/StashesPage.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ namespace SourceGit.ViewModels
99
{
1010
public class StashesPage : ObservableObject, IDisposable
1111
{
12+
public IReadOnlyDictionary<string, string> DecodedPaths
13+
{
14+
get
15+
{
16+
if (_repo == null || !_repo.Settings.EnableUnrealEngineSupport || !_repo.Settings.EnableOFPADecoding)
17+
return null;
18+
return _decodedPaths;
19+
}
20+
}
21+
1222
public List<Models.Stash> Stashes
1323
{
1424
get => _stashes;
@@ -78,6 +88,9 @@ public Models.Stash SelectedStash
7888
{
7989
_untracked = untracked;
8090
Changes = changes;
91+
92+
if (_repo.Settings.EnableUnrealEngineSupport && _repo.Settings.EnableOFPADecoding)
93+
_currentDecodeTask = DecodeOFPAPathsAsync(value, changes, untracked);
8194
}
8295
});
8396
});
@@ -122,10 +135,15 @@ public DiffContext DiffContext
122135
public StashesPage(Repository repo)
123136
{
124137
_repo = repo;
138+
if (_repo != null)
139+
_repo.PropertyChanged += OnRepositoryPropertyChanged;
125140
}
126141

127142
public void Dispose()
128143
{
144+
if (_repo != null)
145+
_repo.PropertyChanged -= OnRepositoryPropertyChanged;
146+
129147
_stashes?.Clear();
130148
_changes?.Clear();
131149
_selectedChanges?.Clear();
@@ -266,6 +284,92 @@ private void RefreshVisible()
266284
}
267285
}
268286

287+
private void OnRepositoryPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
288+
{
289+
if (e.PropertyName == nameof(Repository.EnableUnrealEngineSupport))
290+
{
291+
OnPropertyChanged(nameof(DecodedPaths));
292+
if (_repo.Settings.EnableUnrealEngineSupport && _repo.Settings.EnableOFPADecoding && _selectedStash != null)
293+
_currentDecodeTask = DecodeOFPAPathsAsync(_selectedStash, _changes, _untracked);
294+
}
295+
}
296+
297+
private async Task DecodeOFPAPathsAsync(Models.Stash stash, List<Models.Change> changes, List<Models.Change> untracked)
298+
{
299+
await _currentDecodeTask.ConfigureAwait(false);
300+
301+
if (_repo == null || stash == null ||
302+
!_repo.Settings.EnableUnrealEngineSupport ||
303+
!_repo.Settings.EnableOFPADecoding ||
304+
changes == null || changes.Count == 0)
305+
return;
306+
307+
var repositoryPath = _repo.FullPath;
308+
var untrackedSet = new HashSet<Models.Change>(untracked);
309+
var results = new Dictionary<string, string>(StringComparer.Ordinal);
310+
311+
await Task.Run(async () =>
312+
{
313+
var objectSpecs = new List<(string RelativePath, string Spec)>();
314+
foreach (var change in changes)
315+
{
316+
if (!Utilities.OFPAParser.IsOFPAFile(change.Path))
317+
continue;
318+
319+
string spec;
320+
if (untrackedSet.Contains(change) && stash.Parents.Count == 3)
321+
{
322+
// Untracked files are in the 3rd parent commit.
323+
spec = $"{stash.Parents[2]}:{change.Path}";
324+
}
325+
else
326+
{
327+
// Standard stash changes (index + worktree).
328+
// Deleted files need to be looked up in the parent.
329+
if (change.WorkTree == Models.ChangeState.Deleted || change.Index == Models.ChangeState.Deleted)
330+
spec = $"{stash.Parents[0]}:{change.Path}";
331+
else
332+
spec = $"{stash.SHA}:{change.Path}";
333+
}
334+
335+
objectSpecs.Add((change.Path, spec));
336+
}
337+
338+
if (objectSpecs.Count == 0)
339+
return;
340+
341+
var specs = new List<string>();
342+
foreach (var entry in objectSpecs)
343+
specs.Add(entry.Spec);
344+
345+
var dataMap = await Commands.QueryFileContent.RunBatchAsync(repositoryPath, specs, MaxOFPASampleSize).ConfigureAwait(false);
346+
foreach (var entry in objectSpecs)
347+
{
348+
if (dataMap.TryGetValue(entry.Spec, out var data))
349+
{
350+
var decoded = Utilities.OFPAParser.DecodeFromData(data);
351+
results[entry.RelativePath] = decoded?.LabelValue;
352+
}
353+
}
354+
355+
var updated = new Dictionary<string, string>(StringComparer.Ordinal);
356+
foreach (var kvp in results)
357+
updated[kvp.Key] = kvp.Value;
358+
_decodedPaths = updated;
359+
});
360+
361+
if (results.Count > 0)
362+
{
363+
await Dispatcher.UIThread.InvokeAsync(() =>
364+
{
365+
if (_repo == null || !_repo.Settings.EnableUnrealEngineSupport || !_repo.Settings.EnableOFPADecoding)
366+
return;
367+
368+
OnPropertyChanged(nameof(DecodedPaths));
369+
});
370+
}
371+
}
372+
269373
private Repository _repo = null;
270374
private List<Models.Stash> _stashes = [];
271375
private List<Models.Stash> _visibleStashes = [];
@@ -275,5 +379,8 @@ private void RefreshVisible()
275379
private List<Models.Change> _untracked = [];
276380
private List<Models.Change> _selectedChanges = [];
277381
private DiffContext _diffContext = null;
382+
private Dictionary<string, string> _decodedPaths = null;
383+
private Task _currentDecodeTask = Task.CompletedTask;
384+
private const int MaxOFPASampleSize = 256 * 1024;
278385
}
279386
}

src/Views/CommitChanges.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<v:ChangeCollectionView ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=CommitChangeViewMode}"
4949
EnableCompactFolders="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableCompactFoldersInChangesTree}"
5050
Changes="{Binding VisibleChanges}"
51+
DecodedPaths="{Binding DecodedPaths}"
5152
SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}"
5253
ContextRequested="OnChangeContextRequested"
5354
KeyDown="OnChangeCollectionViewKeyDown"/>

src/Views/CommitDetail.axaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,14 @@
6666
HorizontalAlignment="Left"
6767
Margin="16,0,0,0"
6868
Change="{Binding}"/>
69-
<TextBlock Grid.Column="1" Text="{Binding Path}" Margin="8,0" HorizontalAlignment="Stretch" TextTrimming="CharacterEllipsis"/>
69+
<TextBlock Grid.Column="1" Margin="8,0" HorizontalAlignment="Stretch" TextTrimming="CharacterEllipsis">
70+
<TextBlock.Text>
71+
<MultiBinding Converter="{x:Static c:OFPAConverters.PathToDisplayName}">
72+
<Binding Path="Path"/>
73+
<Binding Path="$parent[UserControl].((vm:CommitDetail)DataContext).DecodedPaths"/>
74+
</MultiBinding>
75+
</TextBlock.Text>
76+
</TextBlock>
7077
</Grid>
7178
</DataTemplate>
7279
</ListBox.ItemTemplate>

src/Views/StashesPage.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
<v:ChangeCollectionView ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=StashChangeViewMode, Mode=OneWay}"
131131
EnableCompactFolders="{Binding Source={x:Static vm:Preferences.Instance}, Path=EnableCompactFoldersInChangesTree}"
132132
Changes="{Binding Changes}"
133+
DecodedPaths="{Binding DecodedPaths}"
133134
SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}"
134135
ContextRequested="OnChangeContextRequested"
135136
KeyDown="OnChangeCollectionViewKeyDown"/>

0 commit comments

Comments
 (0)