diff --git a/GitUI/CommandsDialogs/FormCommit.Designer.cs b/GitUI/CommandsDialogs/FormCommit.Designer.cs
index 42d3d50fa06..4e9f7d387fd 100644
--- a/GitUI/CommandsDialogs/FormCommit.Designer.cs
+++ b/GitUI/CommandsDialogs/FormCommit.Designer.cs
@@ -967,7 +967,7 @@ private void InitializeComponent()
this.toolStageAllItem.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolStageAllItem.Name = "toolStageAllItem";
this.toolStageAllItem.Size = new System.Drawing.Size(23, 23);
- this.toolStageAllItem.Text = "Stage All";
+ this.toolStageAllItem.Text = "Stage all";
this.toolStageAllItem.Click += new System.EventHandler(this.StageAllToolStripMenuItemClick);
//
// toolStripSeparator10
@@ -995,7 +995,7 @@ private void InitializeComponent()
this.toolUnstageAllItem.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolUnstageAllItem.Name = "toolUnstageAllItem";
this.toolUnstageAllItem.Size = new System.Drawing.Size(23, 23);
- this.toolUnstageAllItem.Text = "Unstage All";
+ this.toolUnstageAllItem.Text = "Unstage all";
this.toolUnstageAllItem.Click += new System.EventHandler(this.UnstageAllToolStripMenuItemClick);
//
// toolStripSeparator11
diff --git a/GitUI/CommandsDialogs/FormCommit.cs b/GitUI/CommandsDialogs/FormCommit.cs
index 2ac8c511ed0..26e1f6be12c 100644
--- a/GitUI/CommandsDialogs/FormCommit.cs
+++ b/GitUI/CommandsDialogs/FormCommit.cs
@@ -81,6 +81,8 @@ public sealed partial class FormCommit : GitModuleForm
new TranslationString("There are no files staged for this commit.");
private readonly TranslationString _noFilesStagedButSuggestToCommitAllUnstaged =
new TranslationString("There are no files staged for this commit. Stage and commit all unstaged files?");
+ private readonly TranslationString _noFilesStagedButSuggestToCommitAllFilteredUnstaged =
+ new TranslationString("There are no files staged for this commit. Stage and commit the unstaged files that match your filter?");
private readonly TranslationString _noFilesStagedAndConfirmAnEmptyMergeCommit =
new TranslationString("There are no files staged for this commit.\nAre you sure you want to commit?");
@@ -102,6 +104,11 @@ public sealed partial class FormCommit : GitModuleForm
private readonly TranslationString _stageFiles = new TranslationString("Stage {0} files");
private readonly TranslationString _selectOnlyOneFile = new TranslationString("You must have only one file selected.");
+ private readonly TranslationString _stageAll = new TranslationString("Stage all");
+ private readonly TranslationString _stageFiltered = new TranslationString("Stage filtered");
+ private readonly TranslationString _unstageAll = new TranslationString("Unstage all");
+ private readonly TranslationString _unstageFiltered = new TranslationString("Unstage filtered");
+
private readonly TranslationString _addSelectionToCommitMessage = new TranslationString("Add selection to commit message");
private readonly TranslationString _formTitle = new TranslationString("Commit to {0} ({1})");
@@ -230,6 +237,8 @@ public FormCommit([NotNull] GitUICommands commands, CommitKind commitKind = Comm
Unstaged.SetNoFilesText(_noUnstagedChanges.Text);
Unstaged.DisableSubmoduleMenuItemBold = true;
+ Unstaged.FilterChanged += Unstaged_FilterChanged;
+ Staged.FilterChanged += Staged_FilterChanged;
Staged.SetNoFilesText(_noStagedChanges.Text);
Staged.DisableSubmoduleMenuItemBold = true;
@@ -267,6 +276,8 @@ public FormCommit([NotNull] GitUICommands commands, CommitKind commitKind = Comm
commitAuthorStatus.ToolTipText = _commitCommitterToolTip.Text;
skipWorktreeToolStripMenuItem.ToolTipText = _skipWorktreeToolTip.Text;
assumeUnchangedToolStripMenuItem.ToolTipText = _assumeUnchangedToolTip.Text;
+ toolStageAllItem.ToolTipText = _stageAll.Text;
+ toolUnstageAllItem.ToolTipText = _unstageAll.Text;
stageToolStripMenuItem.Text = toolStageItem.Text;
stageSubmoduleToolStripMenuItem.Text = toolStageItem.Text;
stagedUnstageToolStripMenuItem.Text = toolUnstageItem.Text;
@@ -1345,7 +1356,8 @@ bool ConfirmAndStageAllUnstaged()
}
// there are no staged files, but there are unstaged files. Most probably user forgot to stage them.
- if (MessageBox.Show(this, _noFilesStagedButSuggestToCommitAllUnstaged.Text, _noStagedChanges.Text, MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
+ string message = Unstaged.IsFilterActive ? _noFilesStagedButSuggestToCommitAllFilteredUnstaged.Text : _noFilesStagedButSuggestToCommitAllUnstaged.Text;
+ if (MessageBox.Show(this, message, _noStagedChanges.Text, MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
{
return false;
}
@@ -1653,6 +1665,12 @@ void StageAreaLoaded()
Staged.SelectAll();
Unstage(canUseUnstageAll: false);
}
+ else if (Staged.IsFilterActive)
+ {
+ Staged.SelectedGitItems = Staged.AllItems.Items();
+ Unstage(canUseUnstageAll: false);
+ Staged.SetFilter(string.Empty);
+ }
else
{
Module.Reset(ResetMode.Mixed);
@@ -1742,6 +1760,34 @@ private void Unstaged_Enter(object sender, EnterEventArgs e)
}
}
+ private void Unstaged_FilterChanged(object sender, EventArgs e)
+ {
+ if (Unstaged.IsFilterActive)
+ {
+ toolStageAllItem.ToolTipText = _stageFiltered.Text;
+ toolStageAllItem.Image = Images.StageAllFiltered;
+ }
+ else
+ {
+ toolStageAllItem.ToolTipText = _stageAll.Text;
+ toolStageAllItem.Image = Images.StageAll;
+ }
+ }
+
+ private void Staged_FilterChanged(object sender, EventArgs e)
+ {
+ if (Staged.IsFilterActive)
+ {
+ toolUnstageAllItem.ToolTipText = _unstageFiltered.Text;
+ toolUnstageAllItem.Image = Images.UnstageAllFiltered;
+ }
+ else
+ {
+ toolUnstageAllItem.ToolTipText = _unstageAll.Text;
+ toolUnstageAllItem.Image = Images.UnstageAll;
+ }
+ }
+
private void Unstage(bool canUseUnstageAll = true)
{
if (Module.IsBareRepository())
@@ -3324,8 +3370,14 @@ internal TestAccessor(FormCommit formCommit)
internal ToolStripMenuItem EditFileToolStripMenuItem => _formCommit.editFileToolStripMenuItem;
+ internal ToolStripButton StageAllToolItem => _formCommit.toolStageAllItem;
+
+ internal ToolStripButton UnstageAllToolItem => _formCommit.toolUnstageAllItem;
+
internal FileStatusList UnstagedList => _formCommit.Unstaged;
+ internal FileStatusList StagedList => _formCommit.Staged;
+
internal EditNetSpell Message => _formCommit.Message;
internal FileViewer SelectedDiff => _formCommit.SelectedDiff;
diff --git a/GitUI/Properties/Images.Designer.cs b/GitUI/Properties/Images.Designer.cs
index d55796b1608..2c2046ba13d 100644
--- a/GitUI/Properties/Images.Designer.cs
+++ b/GitUI/Properties/Images.Designer.cs
@@ -1980,6 +1980,16 @@ public static System.Drawing.Bitmap StageAll {
}
}
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ public static System.Drawing.Bitmap StageAllFiltered {
+ get {
+ object obj = ResourceManager.GetObject("StageAllFiltered", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
///
/// Looks up a localized resource of type System.Drawing.Bitmap.
///
@@ -2330,6 +2340,16 @@ public static System.Drawing.Bitmap UnstageAll {
}
}
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ public static System.Drawing.Bitmap UnstageAllFiltered {
+ get {
+ object obj = ResourceManager.GetObject("UnstageAllFiltered", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
///
/// Looks up a localized resource of type System.Drawing.Bitmap.
///
diff --git a/GitUI/Properties/Images.resx b/GitUI/Properties/Images.resx
index addb7168048..d0492a22007 100644
--- a/GitUI/Properties/Images.resx
+++ b/GitUI/Properties/Images.resx
@@ -829,4 +829,10 @@
..\Resources\Icons\pwsh.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+ ..\Resources\Icons\StageAllFiltered.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ ..\Resources\Icons\UnstageAllFiltered.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
\ No newline at end of file
diff --git a/GitUI/Resources/Icons/StageAllFiltered.png b/GitUI/Resources/Icons/StageAllFiltered.png
new file mode 100644
index 00000000000..2b030c03b06
Binary files /dev/null and b/GitUI/Resources/Icons/StageAllFiltered.png differ
diff --git a/GitUI/Resources/Icons/UnstageAllFiltered.png b/GitUI/Resources/Icons/UnstageAllFiltered.png
new file mode 100644
index 00000000000..36b6c5bb05c
Binary files /dev/null and b/GitUI/Resources/Icons/UnstageAllFiltered.png differ
diff --git a/GitUI/Translation/English.xlf b/GitUI/Translation/English.xlf
index 92e43d5be05..aa97ed49558 100644
--- a/GitUI/Translation/English.xlf
+++ b/GitUI/Translation/English.xlf
@@ -3262,6 +3262,10 @@ Are you sure you want to commit?
+
+
+
+
@@ -3309,6 +3313,10 @@ Do you want to continue?
Suitable for some config files modified locally.
+
+
+
+
@@ -3317,6 +3325,10 @@ Suitable for some config files modified locally.
+
+
+
+
@@ -3342,6 +3354,14 @@ You can unset the template:
+
+
+
+
+
+
+
+
@@ -3591,7 +3611,7 @@ You can unset the template:
-
+
@@ -3607,7 +3627,7 @@ You can unset the template:
-
+
diff --git a/GitUI/UserControls/FileStatusList.cs b/GitUI/UserControls/FileStatusList.cs
index 270dc0344fa..71b5826b775 100644
--- a/GitUI/UserControls/FileStatusList.cs
+++ b/GitUI/UserControls/FileStatusList.cs
@@ -52,6 +52,7 @@ public sealed partial class FileStatusList : GitModuleControl
public event EventHandler SelectedIndexChanged;
public event EventHandler DataSourceChanged;
+ public event EventHandler FilterChanged;
public new event EventHandler DoubleClick;
public new event KeyEventHandler KeyDown;
@@ -423,6 +424,8 @@ public IEnumerable FirstGroupItems
public int UnfilteredItemsCount => GitItemStatusesWithDescription?.Sum(tuple => tuple.Statuses.Count) ?? 0;
+ public bool IsFilterActive => !string.IsNullOrEmpty(FilterComboBox.Text);
+
// Public methods
public void ClearSelected()
@@ -1534,6 +1537,7 @@ private void FileStatusList_Enter(object sender, EventArgs e)
private readonly Subject _filterSubject = new Subject();
[CanBeNull] private Regex _filter;
private bool _filterVisible = false;
+ private bool _filterActive = false;
public void SetFilter(string value)
{
@@ -1541,6 +1545,17 @@ public void SetFilter(string value)
FilterFiles(value);
}
+ private void OnFilterChanged()
+ {
+ if (_filterActive == IsFilterActive)
+ {
+ return;
+ }
+
+ _filterActive = IsFilterActive;
+ FilterChanged?.Invoke(this, EventArgs.Empty);
+ }
+
private void DeleteFilterButton_Click(object sender, EventArgs e)
{
SetFilter(string.Empty);
@@ -1551,6 +1566,7 @@ private int FilterFiles(string value)
StoreFilter(value);
UpdateFileStatusListView(updateCausedByFilter: true);
+ OnFilterChanged();
return FileStatusListView.Items.Count;
}
diff --git a/IntegrationTests/UI.IntegrationTests/CommandsDialogs/FormCommitTests.cs b/IntegrationTests/UI.IntegrationTests/CommandsDialogs/FormCommitTests.cs
index aaece33c13d..3ef2b7409d5 100644
--- a/IntegrationTests/UI.IntegrationTests/CommandsDialogs/FormCommitTests.cs
+++ b/IntegrationTests/UI.IntegrationTests/CommandsDialogs/FormCommitTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Drawing;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -207,6 +208,95 @@ public void Should_handle_well_commit_message_in_commit_message_menu()
});
}
+ [SetCulture("en-US")]
+ [SetUICulture("en-US")]
+ [Test]
+ public void Should_stage_only_filtered_on_StageAll()
+ {
+ _referenceRepository.Reset();
+ _referenceRepository.CreateRepoFile("file1A.txt", "Test");
+ _referenceRepository.CreateRepoFile("file1B.txt", "Test");
+ _referenceRepository.CreateRepoFile("file2.txt", "Test");
+
+ RunFormTest(async form =>
+ {
+ using (var cts = new CancellationTokenSource(AsyncTestHelper.UnexpectedTimeout))
+ {
+ await ThreadHelper.JoinPendingOperationsAsync(cts.Token);
+ }
+
+ Assert.AreEqual("Stage all", form.GetTestAccessor().StageAllToolItem.ToolTipText);
+ });
+
+ RunFormTest(async form =>
+ {
+ using (var cts = new CancellationTokenSource(AsyncTestHelper.UnexpectedTimeout))
+ {
+ await ThreadHelper.JoinPendingOperationsAsync(cts.Token);
+ }
+
+ var testform = form.GetTestAccessor();
+
+ testform.UnstagedList.ClearSelected();
+ testform.UnstagedList.SetFilter("file1");
+
+ Assert.AreEqual("Stage filtered", testform.StageAllToolItem.ToolTipText);
+
+ testform.StageAllToolItem.PerformClick();
+
+ var fileNotMatchedByFilterIsStillUnstaged = testform.UnstagedList.AllItems.Where(i => i.Item.Name == "file2.txt").Any();
+
+ Assert.AreEqual(2, testform.StagedList.AllItemsCount);
+ Assert.AreEqual(1, testform.UnstagedList.AllItemsCount);
+ Assert.IsTrue(fileNotMatchedByFilterIsStillUnstaged);
+ });
+ }
+
+ [SetCulture("en-US")]
+ [SetUICulture("en-US")]
+ [Test]
+ public void Should_unstage_only_filtered_on_UnstageAll()
+ {
+ _referenceRepository.Reset();
+ _referenceRepository.CreateRepoFile("file1A.txt", "Test");
+ _referenceRepository.CreateRepoFile("file1B.txt", "Test");
+ _referenceRepository.CreateRepoFile("file2.txt", "Test");
+
+ RunFormTest(async form =>
+ {
+ using (var cts = new CancellationTokenSource(AsyncTestHelper.UnexpectedTimeout))
+ {
+ await ThreadHelper.JoinPendingOperationsAsync(cts.Token);
+ }
+
+ Assert.AreEqual("Unstage all", form.GetTestAccessor().UnstageAllToolItem.ToolTipText);
+ });
+
+ RunFormTest(async form =>
+ {
+ using (var cts = new CancellationTokenSource(AsyncTestHelper.UnexpectedTimeout))
+ {
+ await ThreadHelper.JoinPendingOperationsAsync(cts.Token);
+ }
+
+ var testform = form.GetTestAccessor();
+
+ testform.StageAllToolItem.PerformClick();
+ testform.StagedList.ClearSelected();
+ testform.StagedList.SetFilter("file1");
+
+ Assert.AreEqual("Unstage filtered", testform.UnstageAllToolItem.ToolTipText);
+
+ testform.UnstageAllToolItem.PerformClick();
+
+ var fileNotMatchedByFilterIsStillStaged = testform.StagedList.AllItems.Where(i => i.Item.Name == "file2.txt").Any();
+
+ Assert.AreEqual(2, testform.UnstagedList.AllItemsCount);
+ Assert.AreEqual(1, testform.StagedList.AllItemsCount);
+ Assert.IsTrue(fileNotMatchedByFilterIsStillStaged);
+ });
+ }
+
[Test, TestCaseSource(typeof(CommitMessageTestData), "TestCases")]
public void AddSelectionToCommitMessage_shall_be_ignored_unless_diff_is_focused(
string message,
diff --git a/UnitTests/CommonTestUtils/ReferenceRepository.cs b/UnitTests/CommonTestUtils/ReferenceRepository.cs
index e14e2b73dbb..a9c9be8287b 100644
--- a/UnitTests/CommonTestUtils/ReferenceRepository.cs
+++ b/UnitTests/CommonTestUtils/ReferenceRepository.cs
@@ -51,6 +51,8 @@ public void CreateCommit(string commitMessage, string content = null)
}
}
+ public string CreateRepoFile(string fileName, string fileContent) => _moduleTestHelper.CreateRepoFile(fileName, fileContent);
+
public void CreateTag(string tagName, string commitHash, bool allowOverwrite = false)
{
using (var repository = new LibGit2Sharp.Repository(_moduleTestHelper.Module.WorkingDir))