enhance: supports searching/filtering unstaged changes (#960)

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo 2025-02-08 17:16:56 +08:00
parent 35abeae758
commit 8cc056d2af
No known key found for this signature in database
2 changed files with 165 additions and 81 deletions

View file

@ -99,12 +99,34 @@ namespace SourceGit.ViewModels
} }
} }
public string UnstagedFilter
{
get => _unstagedFilter;
set
{
if (SetProperty(ref _unstagedFilter, value))
{
if (_isLoadingData)
return;
VisibleUnstaged = GetVisibleUnstagedChanges();
SelectedUnstaged = [];
}
}
}
public List<Models.Change> Unstaged public List<Models.Change> Unstaged
{ {
get => _unstaged; get => _unstaged;
private set => SetProperty(ref _unstaged, value); private set => SetProperty(ref _unstaged, value);
} }
public List<Models.Change> VisibleUnstaged
{
get => _visibleUnstaged;
private set => SetProperty(ref _visibleUnstaged, value);
}
public List<Models.Change> Staged public List<Models.Change> Staged
{ {
get => _staged; get => _staged;
@ -191,8 +213,9 @@ namespace SourceGit.ViewModels
_selectedStaged.Clear(); _selectedStaged.Clear();
OnPropertyChanged(nameof(SelectedStaged)); OnPropertyChanged(nameof(SelectedStaged));
_visibleUnstaged.Clear();
_unstaged.Clear(); _unstaged.Clear();
OnPropertyChanged(nameof(Unstaged)); OnPropertyChanged(nameof(VisibleUnstaged));
_staged.Clear(); _staged.Clear();
OnPropertyChanged(nameof(Staged)); OnPropertyChanged(nameof(Staged));
@ -249,7 +272,6 @@ namespace SourceGit.ViewModels
} }
var unstaged = new List<Models.Change>(); var unstaged = new List<Models.Change>();
var selectedUnstaged = new List<Models.Change>();
var hasConflict = false; var hasConflict = false;
foreach (var c in changes) foreach (var c in changes)
{ {
@ -257,12 +279,19 @@ namespace SourceGit.ViewModels
{ {
unstaged.Add(c); unstaged.Add(c);
hasConflict |= c.IsConflit; hasConflict |= c.IsConflit;
if (lastSelectedUnstaged.Contains(c.Path))
selectedUnstaged.Add(c);
} }
} }
_unstaged = unstaged;
var visibleUnstaged = GetVisibleUnstagedChanges();
var selectedUnstaged = new List<Models.Change>();
foreach (var c in visibleUnstaged)
{
if (lastSelectedUnstaged.Contains(c.Path))
selectedUnstaged.Add(c);
}
var staged = GetStagedChanges(); var staged = GetStagedChanges();
var selectedStaged = new List<Models.Change>(); var selectedStaged = new List<Models.Change>();
foreach (var c in staged) foreach (var c in staged)
@ -275,7 +304,7 @@ namespace SourceGit.ViewModels
{ {
_isLoadingData = true; _isLoadingData = true;
HasUnsolvedConflicts = hasConflict; HasUnsolvedConflicts = hasConflict;
Unstaged = unstaged; VisibleUnstaged = visibleUnstaged;
Staged = staged; Staged = staged;
SelectedUnstaged = selectedUnstaged; SelectedUnstaged = selectedUnstaged;
SelectedStaged = selectedStaged; SelectedStaged = selectedStaged;
@ -336,46 +365,7 @@ namespace SourceGit.ViewModels
public void StageAll() public void StageAll()
{ {
StageChanges(_unstaged, null); StageChanges(_visibleUnstaged, null);
}
public async void StageChanges(List<Models.Change> changes, Models.Change next)
{
if (_unstaged.Count == 0 || changes.Count == 0)
return;
// Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh.
_selectedUnstaged = next != null ? [next] : [];
IsStaging = true;
_repo.SetWatcherEnabled(false);
if (changes.Count == _unstaged.Count)
{
await Task.Run(() => new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Exec());
}
else if (Native.OS.GitVersion >= Models.GitVersions.ADD_WITH_PATHSPECFILE)
{
var paths = new List<string>();
foreach (var c in changes)
paths.Add(c.Path);
var tmpFile = Path.GetTempFileName();
File.WriteAllLines(tmpFile, paths);
await Task.Run(() => new Commands.Add(_repo.FullPath, tmpFile).Exec());
File.Delete(tmpFile);
}
else
{
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i);
var step = changes.GetRange(i, count);
await Task.Run(() => new Commands.Add(_repo.FullPath, step).Exec());
}
}
_repo.MarkWorkingCopyDirtyManually();
_repo.SetWatcherEnabled(true);
IsStaging = false;
} }
public void UnstageSelected(Models.Change next) public void UnstageSelected(Models.Change next)
@ -388,44 +378,17 @@ namespace SourceGit.ViewModels
UnstageChanges(_staged, null); UnstageChanges(_staged, null);
} }
public async void UnstageChanges(List<Models.Change> changes, Models.Change next)
{
if (_staged.Count == 0 || changes.Count == 0)
return;
// Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh.
_selectedStaged = next != null ? [next] : [];
IsUnstaging = true;
_repo.SetWatcherEnabled(false);
if (_useAmend)
{
await Task.Run(() => new Commands.UnstageChangesForAmend(_repo.FullPath, changes).Exec());
}
else if (changes.Count == _staged.Count)
{
await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec());
}
else
{
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i);
var step = changes.GetRange(i, count);
await Task.Run(() => new Commands.Reset(_repo.FullPath, step).Exec());
}
}
_repo.MarkWorkingCopyDirtyManually();
_repo.SetWatcherEnabled(true);
IsUnstaging = false;
}
public void Discard(List<Models.Change> changes) public void Discard(List<Models.Change> changes)
{ {
if (_repo.CanCreatePopup()) if (_repo.CanCreatePopup())
_repo.ShowPopup(new Discard(_repo, changes)); _repo.ShowPopup(new Discard(_repo, changes));
} }
public void ClearUnstagedFilter()
{
UnstagedFilter = string.Empty;
}
public async void UseTheirs(List<Models.Change> changes) public async void UseTheirs(List<Models.Change> changes)
{ {
var files = new List<string>(); var files = new List<string>();
@ -1496,6 +1459,22 @@ namespace SourceGit.ViewModels
} }
} }
private List<Models.Change> GetVisibleUnstagedChanges()
{
if (string.IsNullOrEmpty(_unstagedFilter))
return _unstaged;
var visible = new List<Models.Change>();
foreach (var c in _unstaged)
{
if (c.Path.Contains(_unstagedFilter, StringComparison.OrdinalIgnoreCase))
visible.Add(c);
}
return visible;
}
private List<Models.Change> GetStagedChanges() private List<Models.Change> GetStagedChanges()
{ {
if (_useAmend) if (_useAmend)
@ -1511,6 +1490,77 @@ namespace SourceGit.ViewModels
return rs; return rs;
} }
private async void StageChanges(List<Models.Change> changes, Models.Change next)
{
if (changes.Count == 0)
return;
// Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh.
_selectedUnstaged = next != null ? [next] : [];
IsStaging = true;
_repo.SetWatcherEnabled(false);
if (changes.Count == _unstaged.Count)
{
await Task.Run(() => new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Exec());
}
else if (Native.OS.GitVersion >= Models.GitVersions.ADD_WITH_PATHSPECFILE)
{
var paths = new List<string>();
foreach (var c in changes)
paths.Add(c.Path);
var tmpFile = Path.GetTempFileName();
File.WriteAllLines(tmpFile, paths);
await Task.Run(() => new Commands.Add(_repo.FullPath, tmpFile).Exec());
File.Delete(tmpFile);
}
else
{
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i);
var step = changes.GetRange(i, count);
await Task.Run(() => new Commands.Add(_repo.FullPath, step).Exec());
}
}
_repo.MarkWorkingCopyDirtyManually();
_repo.SetWatcherEnabled(true);
IsStaging = false;
}
private async void UnstageChanges(List<Models.Change> changes, Models.Change next)
{
if (changes.Count == 0)
return;
// Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh.
_selectedStaged = next != null ? [next] : [];
IsUnstaging = true;
_repo.SetWatcherEnabled(false);
if (_useAmend)
{
await Task.Run(() => new Commands.UnstageChangesForAmend(_repo.FullPath, changes).Exec());
}
else if (changes.Count == _staged.Count)
{
await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec());
}
else
{
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i);
var step = changes.GetRange(i, count);
await Task.Run(() => new Commands.Reset(_repo.FullPath, step).Exec());
}
}
_repo.MarkWorkingCopyDirtyManually();
_repo.SetWatcherEnabled(true);
IsUnstaging = false;
}
private void SetDetail(Models.Change change, bool isUnstaged) private void SetDetail(Models.Change change, bool isUnstaged)
{ {
if (_isLoadingData) if (_isLoadingData)
@ -1609,11 +1659,13 @@ namespace SourceGit.ViewModels
private bool _hasRemotes = false; private bool _hasRemotes = false;
private List<Models.Change> _cached = []; private List<Models.Change> _cached = [];
private List<Models.Change> _unstaged = []; private List<Models.Change> _unstaged = [];
private List<Models.Change> _visibleUnstaged = [];
private List<Models.Change> _staged = []; private List<Models.Change> _staged = [];
private List<Models.Change> _selectedUnstaged = []; private List<Models.Change> _selectedUnstaged = [];
private List<Models.Change> _selectedStaged = []; private List<Models.Change> _selectedStaged = [];
private int _count = 0; private int _count = 0;
private object _detailContext = null; private object _detailContext = null;
private string _unstagedFilter = string.Empty;
private string _commitMessage = string.Empty; private string _commitMessage = string.Empty;
private bool _hasUnsolvedConflicts = false; private bool _hasUnsolvedConflicts = false;

View file

@ -25,7 +25,7 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Unstaged --> <!-- Unstaged -->
<Grid Grid.Row="0" RowDefinitions="28,*"> <Grid Grid.Row="0" RowDefinitions="28,36,*">
<!-- Unstaged Toolbar --> <!-- Unstaged Toolbar -->
<Border Grid.Row="0" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}"> <Border Grid.Row="0" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
<Grid ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto,Auto,Auto,Auto,Auto"> <Grid ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto,Auto,Auto,Auto,Auto">
@ -75,15 +75,47 @@
</Grid> </Grid>
</Border> </Border>
<!-- Unstaged Filter -->
<Border Grid.Row="1" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}">
<TextBox Height="24"
Margin="4,0"
BorderThickness="1"
CornerRadius="12"
Text="{Binding UnstagedFilter, Mode=TwoWay}"
BorderBrush="{DynamicResource Brush.Border2}"
VerticalContentAlignment="Center">
<TextBox.InnerLeftContent>
<Path Width="14" Height="14"
Margin="6,0,0,0"
Fill="{DynamicResource Brush.FG2}"
Data="{StaticResource Icons.Search}"/>
</TextBox.InnerLeftContent>
<TextBox.InnerRightContent>
<Button Classes="icon_button"
Width="16"
Margin="0,0,6,0"
Command="{Binding ClearUnstagedFilter}"
IsVisible="{Binding UnstagedFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
HorizontalAlignment="Right">
<Path Width="14" Height="14"
Margin="0,1,0,0"
Fill="{DynamicResource Brush.FG1}"
Data="{StaticResource Icons.Clear}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
</Border>
<!-- Unstaged Changes --> <!-- Unstaged Changes -->
<v:ChangeCollectionView Grid.Row="1" <v:ChangeCollectionView Grid.Row="2"
x:Name="UnstagedChangesView" x:Name="UnstagedChangesView"
Focusable="True" Focusable="True"
IsUnstagedChange="True" IsUnstagedChange="True"
SelectionMode="Multiple" SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}" Background="{DynamicResource Brush.Contents}"
ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=UnstagedChangeViewMode}" ViewMode="{Binding Source={x:Static vm:Preferences.Instance}, Path=UnstagedChangeViewMode}"
Changes="{Binding Unstaged}" Changes="{Binding VisibleUnstaged}"
SelectedChanges="{Binding SelectedUnstaged, Mode=TwoWay}" SelectedChanges="{Binding SelectedUnstaged, Mode=TwoWay}"
ContextRequested="OnUnstagedContextRequested" ContextRequested="OnUnstagedContextRequested"
ChangeDoubleTapped="OnUnstagedChangeDoubleTapped" ChangeDoubleTapped="OnUnstagedChangeDoubleTapped"