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? There are no files staged for this commit. + + There are no files staged for this commit. Stage and commit the unstaged files that match your filter? + + There are no files staged for this commit. Stage and commit all unstaged files? @@ -3309,6 +3313,10 @@ Do you want to continue? Suitable for some config files modified locally. + + Stage all + + Stage Details @@ -3317,6 +3325,10 @@ Suitable for some config files modified locally. Stage {0} files + + Stage filtered + + (remote not configured) @@ -3342,6 +3354,14 @@ You can unset the template: Template Error + + Unstage all + + + + Unstage filtered + + (untracked) @@ -3591,7 +3611,7 @@ You can unset the template: - Stage All + Stage all @@ -3607,7 +3627,7 @@ You can unset the template: - Unstage All + Unstage all 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))