From 40a9db17def53643db4b2104d289074b8b41e01c Mon Sep 17 00:00:00 2001 From: RussKie Date: Sun, 6 Sep 2020 22:44:21 +1000 Subject: [PATCH 1/2] Provide ability to sort/order branches Restore the functionality (albeit improved) removed in 9c7c76fb619480429cbe02e11ce889b00423eac2. Allow to either defer sorting to git (i.e. sort by Default) which is either implicit, or configured via `git config branch.sort`. Alternatively expose the following sort by options: - authordate - committerdate - creatordate - taggerdate - refname - objectsize - upstream For user-specified sort, allow to select ascending or descending order (default: desc). Git v2.19+ is required to facilitate sort by default, hence bump in the min version. Update the recommended version to 2.28.0 while we are at it. The sort configuration applies to both the left panel and the branches dropdown in the toolstrip. Fixes #6310 Fixes #8301 Resolves #7472 Closes #4869 Closes #8309 --- .gitignore | 1 + GitCommands/Git/Commands/GitCommandHelpers.cs | 71 +++++++---- GitCommands/Git/GitModule.cs | 51 ++------ GitCommands/Git/GitVersion.cs | 7 +- GitCommands/Settings/AppSettings.cs | 12 ++ .../RepoObjectsTree.Nodes.Branches.cs | 2 +- .../RepoObjectsTree.Nodes.Remotes.cs | 2 +- .../RepoObjectsTree.Nodes.Tags.cs | 4 +- .../BrowseDialog/FormGoToCommit.cs | 4 +- GitUI/CommandsDialogs/FormBrowse.cs | 2 +- GitUI/CommandsDialogs/FormCheckoutBranch.cs | 4 +- GitUI/CommandsDialogs/FormCommit.cs | 2 +- GitUI/CommandsDialogs/FormDeleteBranch.cs | 2 +- GitUI/CommandsDialogs/FormDeleteTag.cs | 2 +- GitUI/CommandsDialogs/FormPush.cs | 5 +- .../Pages/AppearanceSettingsPage.Designer.cs | 114 ++++++++++++++---- .../Pages/AppearanceSettingsPage.cs | 38 +++--- GitUI/Translation/English.xlf | 8 ++ Plugins/GitUIPluginInterfaces/GitRefsOrder.cs | 41 +++++++ .../Git/Commands/GitCommandHelpersTest.cs | 46 ++++++- UnitTests/GitCommands.Tests/GitModuleTests.cs | 1 + 21 files changed, 285 insertions(+), 134 deletions(-) create mode 100644 Plugins/GitUIPluginInterfaces/GitRefsOrder.cs diff --git a/.gitignore b/.gitignore index fbb00dcf90e..c1825d33a04 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,4 @@ tree.txt *.binlog artifacts/ .tools/vswhere/ +.dotnet/ diff --git a/GitCommands/Git/Commands/GitCommandHelpers.cs b/GitCommands/Git/Commands/GitCommandHelpers.cs index 424e88e6832..47db3a6dd9d 100644 --- a/GitCommands/Git/Commands/GitCommandHelpers.cs +++ b/GitCommands/Git/Commands/GitCommandHelpers.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using GitCommands.Git; using GitExtUtils; using GitUIPluginInterfaces; using JetBrains.Annotations; @@ -81,39 +80,59 @@ public static ArgumentString AddSubmoduleCmd(string remotePath, string localPath }; } - public static ArgumentString GetRefsCmd(bool tags, bool branches, bool noLocks) + public static ArgumentString GetRefsCmd(bool tags, bool branches, bool noLocks, GitRefsSortBy sortBy, GitRefsSortOrder sortOrder) { - GitArgumentBuilder cmd; - - if (tags) - { - cmd = new GitArgumentBuilder("show-ref", gitOptions: - noLocks && GitVersion.Current.SupportNoOptionalLocks - ? (ArgumentString)"--no-optional-locks" - : default) - { - { branches, "--dereference", "--tags" }, - }; - } - else if (branches) + string format; + if (!tags) { - // branches only - cmd = new GitArgumentBuilder("for-each-ref", gitOptions: - noLocks && GitVersion.Current.SupportNoOptionalLocks - ? (ArgumentString)"--no-optional-locks" - : default) - { - "--sort=-committerdate", - @"refs/heads/", - @"--format=""%(objectname) %(refname)""" - }; + // If we don't need tags, it is easy. + format = @"--format=""%(objectname) %(refname)"""; } else { - throw new ArgumentException("GetRefs: Neither branches nor tags requested"); + // ...however, if we're interested in tags, tags may be simple (in which case they are point to commits directly), + // or "dereferences" (i.e. commits that contian metadata and point to other commits, "^{}"). + // Derefence commits do not contain date information, so we need to find information from the referenced commits (those with '*'). + // So the following format is as follows: + // If (there is a 'authordate' information, then this is a simple tag/direct commit) + // Then + // format = %(objectname) %(refname) + // Else + // format = %(*objectname) %(*refname) // i.e. info from a referenced commit + // End + format = @"--format=""%(if)%(authordate)%(then)%(objectname) %(refname)%(else)%(*objectname) %(*refname)%(end)"""; } + GitArgumentBuilder cmd = new GitArgumentBuilder("for-each-ref", + gitOptions: noLocks && GitVersion.Current.SupportNoOptionalLocks + ? (ArgumentString)"--no-optional-locks" + : default) + { + { sortBy != GitRefsSortBy.Default, GetSortCriteria(tags, sortBy, sortOrder), string.Empty }, + format, + { branches, "refs/heads/", string.Empty }, + { branches && tags, "refs/remotes/", string.Empty }, + { tags, "refs/tags/", string.Empty }, + }; + return cmd; + + static string GetSortCriteria(bool needTags, GitRefsSortBy sortBy, GitRefsSortOrder sortOrder) + { + if (!GitVersion.Current.SupportRefSort) + { + return string.Empty; + } + + string order = sortOrder == GitRefsSortOrder.Ascending ? string.Empty : "-"; + if (!needTags) + { + return $"--sort={order}{sortBy}"; + } + + // Sort by dereferenced data + return $"--sort={order}{sortBy} --sort={order}*{sortBy}"; + } } public static ArgumentString RevertCmd(ObjectId commitId, bool autoCommit, int parentIndex) diff --git a/GitCommands/Git/GitModule.cs b/GitCommands/Git/GitModule.cs index c504a115dd0..b9a99ae8238 100644 --- a/GitCommands/Git/GitModule.cs +++ b/GitCommands/Git/GitModule.cs @@ -2895,53 +2895,18 @@ public RemoteActionResult> GetRemoteServerRefs(string rem public IReadOnlyList GetRefs(bool tags = true, bool branches = true) { - return GetRefs(tags, branches, false); - } - - public IReadOnlyList GetRefs(bool tags, bool branches, bool noLocks) - { - var refList = _gitExecutable.GetOutput(GitCommandHelpers.GetRefsCmd(tags: tags, branches: branches, noLocks: noLocks)); + // We do not want to lock the repo for background operations. + // The primary use of 'noLocks' is to run git-status the commit count as a background operation, + // but to run the same in a foreground for FormCommit. + // + // Assume that all GetRefs() are done in the background, which may not be correct in the future. + const bool noLocks = true; + string cmd = GitCommandHelpers.GetRefsCmd(tags: tags, branches: branches, noLocks, AppSettings.RefsSortBy, AppSettings.RefsSortOrder); + var refList = _gitExecutable.GetOutput(cmd); return ParseRefs(refList); } - /// Order by date is slower. - public IReadOnlyList GetTagRefs(GetTagRefsSortOrder option) - { - var list = GetRefs(true, false); - - switch (option) - { - case GetTagRefsSortOrder.ByCommitDateAscending: - return list.OrderBy(GetDate).ToList(); - case GetTagRefsSortOrder.ByCommitDateDescending: - return list.OrderByDescending(GetDate).ToList(); - default: - return list; - } - - // BUG this sorting logic has no effect as CommitDate is not set by the GitRevision constructor - DateTime GetDate(IGitRef head) => new GitRevision(head.ObjectId).CommitDate; - } - - public enum GetTagRefsSortOrder - { - /// - /// default - /// - ByName, - - /// - /// slower than ByName - /// - ByCommitDateAscending, - - /// - /// slower than ByName - /// - ByCommitDateDescending - } - public async Task GetMergedBranchesAsync(bool includeRemote = false, bool fullRefname = false, string commit = null) => (await _gitExecutable .GetOutputAsync(GitCommandHelpers.MergedBranchesCmd(includeRemote, fullRefname, commit)) diff --git a/GitCommands/Git/GitVersion.cs b/GitCommands/Git/GitVersion.cs index 68088d1f1ce..aaeb6fb6c5f 100644 --- a/GitCommands/Git/GitVersion.cs +++ b/GitCommands/Git/GitVersion.cs @@ -17,13 +17,14 @@ public class GitVersion : IComparable private static readonly GitVersion v2_7_0 = new GitVersion("2.7.0"); private static readonly GitVersion v2_9_0 = new GitVersion("2.9.0"); private static readonly GitVersion v2_11_0 = new GitVersion("2.11.0"); + private static readonly GitVersion v2_14_6 = new GitVersion("2.14.6"); private static readonly GitVersion v2_15_0 = new GitVersion("2.15.0"); private static readonly GitVersion v2_15_2 = new GitVersion("2.15.2"); private static readonly GitVersion v2_19_0 = new GitVersion("2.19.0"); private static readonly GitVersion v2_20_0 = new GitVersion("2.20.0"); - public static readonly GitVersion LastSupportedVersion = v2_11_0; - public static readonly GitVersion LastRecommendedVersion = new GitVersion("2.25.1"); + public static readonly GitVersion LastSupportedVersion = v2_19_0; + public static readonly GitVersion LastRecommendedVersion = new GitVersion("2.28.0"); private static GitVersion _current; @@ -128,6 +129,8 @@ int Get(IReadOnlyList values, int index) public bool SupportRebaseMerges => this >= v2_19_0; + public bool SupportRefSort => this >= v2_14_6; + public bool SupportGuiMergeTool => this >= v2_20_0; public bool IsUnknown => _a == 0 && _b == 0 && _c == 0 && _d == 0; diff --git a/GitCommands/Settings/AppSettings.cs b/GitCommands/Settings/AppSettings.cs index 23efd93b95e..791db556666 100644 --- a/GitCommands/Settings/AppSettings.cs +++ b/GitCommands/Settings/AppSettings.cs @@ -1579,6 +1579,18 @@ public static bool UseConsoleEmulatorForCommands set => SetBool("UseConsoleEmulatorForCommands", value); } + public static GitRefsSortBy RefsSortBy + { + get => GetEnum("RefsSortBy", GitRefsSortBy.Default); + set => SetEnum("RefsSortBy", value); + } + + public static GitRefsSortOrder RefsSortOrder + { + get => GetEnum("RefsSortOrder", GitRefsSortOrder.Descending); + set => SetEnum("RefsSortOrder", value); + } + public static DiffListSortType DiffListSorting { get => GetEnum("DiffListSortType", DiffListSortType.FilePath); diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs index 4789be158d7..f56bb42ab72 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs @@ -279,7 +279,7 @@ private async Task LoadNodesAsync(CancellationToken token) await TaskScheduler.Default; token.ThrowIfCancellationRequested(); - var branchNames = Module.GetRefs(tags: false, branches: true, noLocks: true).Select(b => b.Name); + var branchNames = Module.GetRefs(tags: false, branches: true).Select(b => b.Name); return FillBranchTree(branchNames, token); } diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs index f607d1a2bf0..b926df06b76 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs @@ -44,7 +44,7 @@ private async Task LoadNodesAsync(CancellationToken token) var nodes = new Nodes(this); var pathToNodes = new Dictionary(); - var branches = Module.GetRefs(tags: true, branches: true, noLocks: true) + var branches = Module.GetRefs(tags: true, branches: true) .Where(branch => branch.IsRemote && !branch.IsTag) .OrderBy(branch => branch.Name) .Select(branch => branch.Name); diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs index 4e763a94ee3..55c3d6214d3 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs @@ -2,13 +2,11 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using GitCommands; using GitUI.BranchTreePanel.Interfaces; using GitUI.CommandsDialogs; using GitUI.Properties; using GitUIPluginInterfaces; using Microsoft.VisualStudio.Threading; -using ResourceManager; namespace GitUI.BranchTreePanel { @@ -91,7 +89,7 @@ private async Task LoadNodesAsync(CancellationToken token) { await TaskScheduler.Default; token.ThrowIfCancellationRequested(); - return FillTagTree(Module.GetTagRefs(GitModule.GetTagRefsSortOrder.ByName), token); + return FillTagTree(Module.GetRefs(tags: true, branches: false), token); } private Nodes FillTagTree(IEnumerable tags, CancellationToken token) diff --git a/GitUI/CommandsDialogs/BrowseDialog/FormGoToCommit.cs b/GitUI/CommandsDialogs/BrowseDialog/FormGoToCommit.cs index 3d9493a94a3..0a4cb079c0f 100644 --- a/GitUI/CommandsDialogs/BrowseDialog/FormGoToCommit.cs +++ b/GitUI/CommandsDialogs/BrowseDialog/FormGoToCommit.cs @@ -78,7 +78,7 @@ private Task LoadTagsAsync() { comboBoxTags.Text = Strings.LoadingData; return _tagsLoader.LoadAsync( - () => Module.GetTagRefs(GitModule.GetTagRefsSortOrder.ByCommitDateDescending).ToList(), + () => Module.GetRefs(tags: true, branches: false).ToList(), list => { comboBoxTags.Text = string.Empty; @@ -92,7 +92,7 @@ private Task LoadBranchesAsync() { comboBoxBranches.Text = Strings.LoadingData; return _branchesLoader.LoadAsync( - () => Module.GetRefs(false).ToList(), + () => Module.GetRefs(tags: false, branches: true).ToList(), list => { comboBoxBranches.Text = string.Empty; diff --git a/GitUI/CommandsDialogs/FormBrowse.cs b/GitUI/CommandsDialogs/FormBrowse.cs index 9ef33f290d6..c9422242c14 100644 --- a/GitUI/CommandsDialogs/FormBrowse.cs +++ b/GitUI/CommandsDialogs/FormBrowse.cs @@ -2025,7 +2025,7 @@ IEnumerable GetBranchNames() // Make sure there are never more than a 100 branches added to the menu // Git Extensions will hang when the drop down is too large... return Module - .GetRefs(tags: false, branches: true, noLocks: true) + .GetRefs(tags: false, branches: true) .Select(b => b.Name) .Take(100); } diff --git a/GitUI/CommandsDialogs/FormCheckoutBranch.cs b/GitUI/CommandsDialogs/FormCheckoutBranch.cs index 24b2f1b9c2f..544573197dd 100644 --- a/GitUI/CommandsDialogs/FormCheckoutBranch.cs +++ b/GitUI/CommandsDialogs/FormCheckoutBranch.cs @@ -524,7 +524,7 @@ private IEnumerable GetLocalBranches() { if (_localBranches == null) { - _localBranches = Module.GetRefs(false); + _localBranches = Module.GetRefs(tags: false, branches: true); } return _localBranches; @@ -534,7 +534,7 @@ private IEnumerable GetRemoteBranches() { if (_remoteBranches == null) { - _remoteBranches = Module.GetRefs(true, true).Where(h => h.IsRemote && !h.IsTag).ToList(); + _remoteBranches = Module.GetRefs(tags: true, branches: true).Where(h => h.IsRemote && !h.IsTag).ToList(); } return _remoteBranches; diff --git a/GitUI/CommandsDialogs/FormCommit.cs b/GitUI/CommandsDialogs/FormCommit.cs index 3dc1a48eb50..b7769b5614d 100644 --- a/GitUI/CommandsDialogs/FormCommit.cs +++ b/GitUI/CommandsDialogs/FormCommit.cs @@ -1030,7 +1030,7 @@ private async Task UpdateBranchNameDisplayAsync() remoteNameLabel.Click -= _branchNameLabelOnClick; } - var currentBranch = Module.GetRefs(false, true).FirstOrDefault(r => r.LocalName == currentBranchName); + var currentBranch = Module.GetRefs(tags: false, branches: true).FirstOrDefault(r => r.LocalName == currentBranchName); if (currentBranch == null) { branchNameLabel.Text = currentBranchName; diff --git a/GitUI/CommandsDialogs/FormDeleteBranch.cs b/GitUI/CommandsDialogs/FormDeleteBranch.cs index 8e98a2a4741..a670804b80a 100644 --- a/GitUI/CommandsDialogs/FormDeleteBranch.cs +++ b/GitUI/CommandsDialogs/FormDeleteBranch.cs @@ -40,7 +40,7 @@ public FormDeleteBranch(GitUICommands commands, IEnumerable defaultBranc private void FormDeleteBranchLoad(object sender, EventArgs e) { - Branches.BranchesToSelect = Module.GetRefs(true, true).Where(h => h.IsHead && !h.IsRemote).ToList(); + Branches.BranchesToSelect = Module.GetRefs(tags: true, branches: true).Where(h => h.IsHead && !h.IsRemote).ToList(); foreach (var branch in Module.GetMergedBranches()) { if (!branch.StartsWith("* ")) diff --git a/GitUI/CommandsDialogs/FormDeleteTag.cs b/GitUI/CommandsDialogs/FormDeleteTag.cs index 6859a1d9410..0d08fef4143 100644 --- a/GitUI/CommandsDialogs/FormDeleteTag.cs +++ b/GitUI/CommandsDialogs/FormDeleteTag.cs @@ -33,7 +33,7 @@ public FormDeleteTag(GitUICommands commands, string tag) private void FormDeleteTagLoad(object sender, EventArgs e) { Tags.DisplayMember = nameof(IGitRef.LocalName); - Tags.DataSource = Module.GetRefs(true, false); + Tags.DataSource = Module.GetRefs(tags: true, branches: false); Tags.Text = Tag as string; remotesComboboxControl1.SelectedRemote = Module.GetCurrentRemote(); EnableOrDisableRemotesCombobox(); diff --git a/GitUI/CommandsDialogs/FormPush.cs b/GitUI/CommandsDialogs/FormPush.cs index eb4f519a58b..337e3e3e523 100644 --- a/GitUI/CommandsDialogs/FormPush.cs +++ b/GitUI/CommandsDialogs/FormPush.cs @@ -907,8 +907,9 @@ private void RemotesValidated(object sender, EventArgs e) private void FillTagDropDown() { // var tags = Module.GetTagHeads(GitModule.GetTagHeadsOption.OrderByCommitDateDescending); // comment out to sort by commit date - var tags = Module.GetTagRefs(GitModule.GetTagRefsSortOrder.ByName) - .Select(tag => tag.Name).ToList(); + List tags = Module.GetRefs(tags: true, branches: false) + .Select(tag => tag.Name) + .ToList(); tags.Insert(0, AllRefs); TagComboBox.DataSource = tags; diff --git a/GitUI/CommandsDialogs/SettingsDialog/Pages/AppearanceSettingsPage.Designer.cs b/GitUI/CommandsDialogs/SettingsDialog/Pages/AppearanceSettingsPage.Designer.cs index 622cae4e779..63716cec8d4 100644 --- a/GitUI/CommandsDialogs/SettingsDialog/Pages/AppearanceSettingsPage.Designer.cs +++ b/GitUI/CommandsDialogs/SettingsDialog/Pages/AppearanceSettingsPage.Designer.cs @@ -61,6 +61,10 @@ private void InitializeComponent() this.fixedWidthFontDialog = new System.Windows.Forms.FontDialog(); this.applicationDialog = new System.Windows.Forms.FontDialog(); this.commitFontDialog = new System.Windows.Forms.FontDialog(); + this.lblBranchesSortBy = new System.Windows.Forms.Label(); + this.lblBranchesOrder = new System.Windows.Forms.Label(); + this._NO_TRANSLATE_cmbBranchesSortBy = new System.Windows.Forms.ComboBox(); + this._NO_TRANSLATE_cmbBranchesOrder = new System.Windows.Forms.ComboBox(); tlpnlMain = new System.Windows.Forms.TableLayoutPanel(); tlpnlMain.SuspendLayout(); this.gbGeneral.SuspendLayout(); @@ -89,7 +93,7 @@ private void InitializeComponent() tlpnlMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); tlpnlMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); tlpnlMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 10F)); - tlpnlMain.Size = new System.Drawing.Size(1542, 481); + tlpnlMain.Size = new System.Drawing.Size(1565, 1339); tlpnlMain.TabIndex = 0; // // gbGeneral @@ -101,7 +105,7 @@ private void InitializeComponent() this.gbGeneral.Location = new System.Drawing.Point(3, 3); this.gbGeneral.Name = "gbGeneral"; this.gbGeneral.Padding = new System.Windows.Forms.Padding(8); - this.gbGeneral.Size = new System.Drawing.Size(1536, 125); + this.gbGeneral.Size = new System.Drawing.Size(1559, 225); this.gbGeneral.TabIndex = 0; this.gbGeneral.TabStop = false; this.gbGeneral.Text = "General"; @@ -114,24 +118,30 @@ private void InitializeComponent() this.tlpnlGeneral.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tlpnlGeneral.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tlpnlGeneral.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tlpnlGeneral.Controls.Add(this._NO_TRANSLATE_cmbBranchesOrder, 0, 6); + this.tlpnlGeneral.Controls.Add(this._NO_TRANSLATE_cmbBranchesSortBy, 0, 5); + this.tlpnlGeneral.Controls.Add(this.lblBranchesOrder, 0, 6); + this.tlpnlGeneral.Controls.Add(this.lblBranchesSortBy, 0, 5); this.tlpnlGeneral.Controls.Add(this.chkShowRelativeDate, 0, 0); this.tlpnlGeneral.Controls.Add(this.chkShowRepoCurrentBranch, 0, 1); this.tlpnlGeneral.Controls.Add(this.chkShowCurrentBranchInVisualStudio, 0, 2); this.tlpnlGeneral.Controls.Add(this.chkEnableAutoScale, 0, 3); this.tlpnlGeneral.Controls.Add(this.chkSortByAuthorDate, 0, 4); - this.tlpnlGeneral.Controls.Add(this.truncateLongFilenames, 0, 5); - this.tlpnlGeneral.Controls.Add(this.truncatePathMethod, 1, 5); + this.tlpnlGeneral.Controls.Add(this.truncateLongFilenames, 0, 7); + this.tlpnlGeneral.Controls.Add(this.truncatePathMethod, 1, 7); this.tlpnlGeneral.Dock = System.Windows.Forms.DockStyle.Fill; this.tlpnlGeneral.Location = new System.Drawing.Point(8, 21); this.tlpnlGeneral.Name = "tlpnlGeneral"; - this.tlpnlGeneral.RowCount = 6; + this.tlpnlGeneral.RowCount = 8; this.tlpnlGeneral.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tlpnlGeneral.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tlpnlGeneral.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tlpnlGeneral.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tlpnlGeneral.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tlpnlGeneral.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tlpnlGeneral.Size = new System.Drawing.Size(1035, 119); + this.tlpnlGeneral.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tlpnlGeneral.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tlpnlGeneral.Size = new System.Drawing.Size(1543, 196); this.tlpnlGeneral.TabIndex = 0; // // chkShowRelativeDate @@ -141,7 +151,7 @@ private void InitializeComponent() this.chkShowRelativeDate.Dock = System.Windows.Forms.DockStyle.Fill; this.chkShowRelativeDate.Location = new System.Drawing.Point(3, 3); this.chkShowRelativeDate.Name = "chkShowRelativeDate"; - this.chkShowRelativeDate.Size = new System.Drawing.Size(314, 17); + this.chkShowRelativeDate.Size = new System.Drawing.Size(448, 17); this.chkShowRelativeDate.TabIndex = 0; this.chkShowRelativeDate.Text = "Show relative date instead of full date"; this.chkShowRelativeDate.UseVisualStyleBackColor = true; @@ -153,7 +163,7 @@ private void InitializeComponent() this.chkShowRepoCurrentBranch.Dock = System.Windows.Forms.DockStyle.Fill; this.chkShowRepoCurrentBranch.Location = new System.Drawing.Point(3, 26); this.chkShowRepoCurrentBranch.Name = "chkShowRepoCurrentBranch"; - this.chkShowRepoCurrentBranch.Size = new System.Drawing.Size(314, 17); + this.chkShowRepoCurrentBranch.Size = new System.Drawing.Size(448, 17); this.chkShowRepoCurrentBranch.TabIndex = 5; this.chkShowRepoCurrentBranch.Text = "Show current branch names in the dashboard and the recent repositories dropdown menu"; this.chkShowRepoCurrentBranch.UseVisualStyleBackColor = true; @@ -165,7 +175,7 @@ private void InitializeComponent() this.chkShowCurrentBranchInVisualStudio.Dock = System.Windows.Forms.DockStyle.Fill; this.chkShowCurrentBranchInVisualStudio.Location = new System.Drawing.Point(3, 49); this.chkShowCurrentBranchInVisualStudio.Name = "chkShowCurrentBranchInVisualStudio"; - this.chkShowCurrentBranchInVisualStudio.Size = new System.Drawing.Size(314, 17); + this.chkShowCurrentBranchInVisualStudio.Size = new System.Drawing.Size(448, 17); this.chkShowCurrentBranchInVisualStudio.TabIndex = 1; this.chkShowCurrentBranchInVisualStudio.Text = "Show current branch in Visual Studio"; this.chkShowCurrentBranchInVisualStudio.UseVisualStyleBackColor = true; @@ -175,9 +185,9 @@ private void InitializeComponent() this.chkEnableAutoScale.AutoSize = true; this.tlpnlGeneral.SetColumnSpan(this.chkEnableAutoScale, 2); this.chkEnableAutoScale.Dock = System.Windows.Forms.DockStyle.Fill; - this.chkEnableAutoScale.Location = new System.Drawing.Point(3, 69); + this.chkEnableAutoScale.Location = new System.Drawing.Point(3, 72); this.chkEnableAutoScale.Name = "chkEnableAutoScale"; - this.chkEnableAutoScale.Size = new System.Drawing.Size(314, 17); + this.chkEnableAutoScale.Size = new System.Drawing.Size(448, 17); this.chkEnableAutoScale.TabIndex = 2; this.chkEnableAutoScale.Text = "Auto scale user interface when high DPI is used"; this.chkEnableAutoScale.UseVisualStyleBackColor = true; @@ -185,9 +195,11 @@ private void InitializeComponent() // chkSortByAuthorDate // this.chkSortByAuthorDate.AutoSize = true; - this.chkSortByAuthorDate.Location = new System.Drawing.Point(3, 89); + this.chkSortByAuthorDate.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.chkSortByAuthorDate.Checked = false; + this.chkSortByAuthorDate.Location = new System.Drawing.Point(3, 95); this.chkSortByAuthorDate.Name = "chkSortByAuthorDate"; - this.chkSortByAuthorDate.Size = new System.Drawing.Size(116, 17); + this.chkSortByAuthorDate.Size = new System.Drawing.Size(118, 17); this.chkSortByAuthorDate.TabIndex = 3; this.chkSortByAuthorDate.Text = "Sort by author date"; // @@ -195,7 +207,7 @@ private void InitializeComponent() // this.truncateLongFilenames.AutoSize = true; this.truncateLongFilenames.Dock = System.Windows.Forms.DockStyle.Fill; - this.truncateLongFilenames.Location = new System.Drawing.Point(3, 109); + this.truncateLongFilenames.Location = new System.Drawing.Point(3, 169); this.truncateLongFilenames.Name = "truncateLongFilenames"; this.truncateLongFilenames.Size = new System.Drawing.Size(120, 27); this.truncateLongFilenames.TabIndex = 4; @@ -212,9 +224,9 @@ private void InitializeComponent() "Compact", "Trim start", "Filename only"}); - this.truncatePathMethod.Location = new System.Drawing.Point(129, 72); + this.truncatePathMethod.Location = new System.Drawing.Point(129, 172); this.truncatePathMethod.Name = "truncatePathMethod"; - this.truncatePathMethod.Size = new System.Drawing.Size(188, 21); + this.truncatePathMethod.Size = new System.Drawing.Size(322, 21); this.truncatePathMethod.TabIndex = 4; // // gbAuthorImages @@ -223,10 +235,10 @@ private void InitializeComponent() this.gbAuthorImages.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; this.gbAuthorImages.Controls.Add(this.tlpnlAuthor); this.gbAuthorImages.Dock = System.Windows.Forms.DockStyle.Fill; - this.gbAuthorImages.Location = new System.Drawing.Point(3, 134); + this.gbAuthorImages.Location = new System.Drawing.Point(3, 234); this.gbAuthorImages.Name = "gbAuthorImages"; this.gbAuthorImages.Padding = new System.Windows.Forms.Padding(8); - this.gbAuthorImages.Size = new System.Drawing.Size(1536, 184); + this.gbAuthorImages.Size = new System.Drawing.Size(1559, 184); this.gbAuthorImages.TabIndex = 1; this.gbAuthorImages.TabStop = false; this.gbAuthorImages.Text = "Author images"; @@ -259,7 +271,7 @@ private void InitializeComponent() this.tlpnlAuthor.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tlpnlAuthor.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tlpnlAuthor.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tlpnlAuthor.Size = new System.Drawing.Size(1520, 155); + this.tlpnlAuthor.Size = new System.Drawing.Size(1543, 155); this.tlpnlAuthor.TabIndex = 0; // // lblAvatarProvider @@ -327,10 +339,10 @@ private void InitializeComponent() this.gbLanguages.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; this.gbLanguages.Controls.Add(this.tlpnlLanguage); this.gbLanguages.Dock = System.Windows.Forms.DockStyle.Fill; - this.gbLanguages.Location = new System.Drawing.Point(3, 324); + this.gbLanguages.Location = new System.Drawing.Point(3, 424); this.gbLanguages.Name = "gbLanguages"; this.gbLanguages.Padding = new System.Windows.Forms.Padding(8); - this.gbLanguages.Size = new System.Drawing.Size(1536, 83); + this.gbLanguages.Size = new System.Drawing.Size(1559, 83); this.gbLanguages.TabIndex = 2; this.gbLanguages.TabStop = false; this.gbLanguages.Text = "Language"; @@ -356,7 +368,7 @@ private void InitializeComponent() this.tlpnlLanguage.RowCount = 2; this.tlpnlLanguage.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tlpnlLanguage.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tlpnlLanguage.Size = new System.Drawing.Size(1520, 54); + this.tlpnlLanguage.Size = new System.Drawing.Size(1543, 54); this.tlpnlLanguage.TabIndex = 0; // // Dictionary @@ -501,6 +513,58 @@ private void InitializeComponent() this.commitFontDialog.AllowVerticalFonts = false; this.commitFontDialog.Color = System.Drawing.SystemColors.ControlText; // + // lblBranchesSortBy + // + this.lblBranchesSortBy.AutoSize = true; + this.lblBranchesSortBy.Dock = System.Windows.Forms.DockStyle.Fill; + this.lblBranchesSortBy.Location = new System.Drawing.Point(3, 115); + this.lblBranchesSortBy.Name = "lblBranchesSortBy"; + this.lblBranchesSortBy.Size = new System.Drawing.Size(120, 27); + this.lblBranchesSortBy.TabIndex = 6; + this.lblBranchesSortBy.Text = "Sort branches by"; + this.lblBranchesSortBy.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // lblBranchesOrder + // + this.lblBranchesOrder.AutoSize = true; + this.lblBranchesOrder.Dock = System.Windows.Forms.DockStyle.Fill; + this.lblBranchesOrder.Location = new System.Drawing.Point(3, 142); + this.lblBranchesOrder.Name = "lblBranchesOrder"; + this.lblBranchesOrder.Size = new System.Drawing.Size(120, 27); + this.lblBranchesOrder.TabIndex = 7; + this.lblBranchesOrder.Text = "Order branches"; + this.lblBranchesOrder.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // _NO_TRANSLATE_cmbBranchesSortBy + // + this._NO_TRANSLATE_cmbBranchesSortBy.Dock = System.Windows.Forms.DockStyle.Fill; + this._NO_TRANSLATE_cmbBranchesSortBy.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this._NO_TRANSLATE_cmbBranchesSortBy.FormattingEnabled = true; + this._NO_TRANSLATE_cmbBranchesSortBy.Items.AddRange(new object[] { + "None", + "Compact", + "Trim start", + "Filename only"}); + this._NO_TRANSLATE_cmbBranchesSortBy.Location = new System.Drawing.Point(129, 118); + this._NO_TRANSLATE_cmbBranchesSortBy.Name = "_NO_TRANSLATE_cmbBranchesSortBy"; + this._NO_TRANSLATE_cmbBranchesSortBy.Size = new System.Drawing.Size(322, 21); + this._NO_TRANSLATE_cmbBranchesSortBy.TabIndex = 8; + // + // _NO_TRANSLATE_cmbBranchesOrder + // + this._NO_TRANSLATE_cmbBranchesOrder.Dock = System.Windows.Forms.DockStyle.Fill; + this._NO_TRANSLATE_cmbBranchesOrder.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this._NO_TRANSLATE_cmbBranchesOrder.FormattingEnabled = true; + this._NO_TRANSLATE_cmbBranchesOrder.Items.AddRange(new object[] { + "None", + "Compact", + "Trim start", + "Filename only"}); + this._NO_TRANSLATE_cmbBranchesOrder.Location = new System.Drawing.Point(129, 145); + this._NO_TRANSLATE_cmbBranchesOrder.Name = "_NO_TRANSLATE_cmbBranchesOrder"; + this._NO_TRANSLATE_cmbBranchesOrder.Size = new System.Drawing.Size(322, 21); + this._NO_TRANSLATE_cmbBranchesOrder.TabIndex = 9; + // // AppearanceSettingsPage // this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); @@ -509,7 +573,7 @@ private void InitializeComponent() this.MinimumSize = new System.Drawing.Size(258, 255); this.Name = "AppearanceSettingsPage"; this.Padding = new System.Windows.Forms.Padding(8); - this.Size = new System.Drawing.Size(1558, 497); + this.Size = new System.Drawing.Size(1581, 1355); tlpnlMain.ResumeLayout(false); tlpnlMain.PerformLayout(); this.gbGeneral.ResumeLayout(false); @@ -564,5 +628,9 @@ private void InitializeComponent() private System.Windows.Forms.Label lblAvatarProvider; private System.Windows.Forms.ComboBox AvatarProvider; private GitUI.UserControls.Settings.SettingsCheckBox chkSortByAuthorDate; + private System.Windows.Forms.Label lblBranchesOrder; + private System.Windows.Forms.Label lblBranchesSortBy; + private System.Windows.Forms.ComboBox _NO_TRANSLATE_cmbBranchesSortBy; + private System.Windows.Forms.ComboBox _NO_TRANSLATE_cmbBranchesOrder; } } diff --git a/GitUI/CommandsDialogs/SettingsDialog/Pages/AppearanceSettingsPage.cs b/GitUI/CommandsDialogs/SettingsDialog/Pages/AppearanceSettingsPage.cs index 0b2203ade57..29e690d1894 100644 --- a/GitUI/CommandsDialogs/SettingsDialog/Pages/AppearanceSettingsPage.cs +++ b/GitUI/CommandsDialogs/SettingsDialog/Pages/AppearanceSettingsPage.cs @@ -7,6 +7,7 @@ using GitCommands.Utils; using GitExtUtils.GitUI; using GitUI.Avatars; +using GitUIPluginInterfaces; using ResourceManager; namespace GitUI.CommandsDialogs.SettingsDialog.Pages @@ -24,6 +25,8 @@ public AppearanceSettingsPage() Text = "Appearance"; InitializeComplete(); + FillComboBoxWithEnumValues(_NO_TRANSLATE_cmbBranchesOrder); + FillComboBoxWithEnumValues(_NO_TRANSLATE_cmbBranchesSortBy); FillComboBoxWithEnumValues(AvatarProvider); FillComboBoxWithEnumValues(_NO_TRANSLATE_NoImageService); } @@ -47,16 +50,16 @@ protected override void OnRuntimeLoad() pictureAvatarHelp.Size = DpiUtil.Scale(pictureAvatarHelp.Size); // align 1st columns across all tables - tlpnlGeneral.AdjustWidthToSize(0, truncateLongFilenames, lblCacheDays, lblNoImageService, lblLanguage, lblSpellingDictionary); - tlpnlAuthor.AdjustWidthToSize(0, truncateLongFilenames, lblCacheDays, lblNoImageService, lblLanguage, lblSpellingDictionary); - tlpnlLanguage.AdjustWidthToSize(0, truncateLongFilenames, lblCacheDays, lblNoImageService, lblLanguage, lblSpellingDictionary); + tlpnlGeneral.AdjustWidthToSize(0, lblBranchesSortBy, lblBranchesOrder, truncateLongFilenames, lblCacheDays, lblNoImageService, lblLanguage, lblSpellingDictionary); + tlpnlAuthor.AdjustWidthToSize(0, lblBranchesSortBy, lblBranchesOrder, truncateLongFilenames, lblCacheDays, lblNoImageService, lblLanguage, lblSpellingDictionary); + tlpnlLanguage.AdjustWidthToSize(0, lblBranchesSortBy, lblBranchesOrder, truncateLongFilenames, lblCacheDays, lblNoImageService, lblLanguage, lblSpellingDictionary); // align 2nd columns across all tables truncatePathMethod.AdjustWidthToFitContent(); Language.AdjustWidthToFitContent(); - tlpnlGeneral.AdjustWidthToSize(1, truncatePathMethod, _NO_TRANSLATE_NoImageService, Language); - tlpnlAuthor.AdjustWidthToSize(1, truncatePathMethod, _NO_TRANSLATE_NoImageService, Language); - tlpnlLanguage.AdjustWidthToSize(1, truncatePathMethod, _NO_TRANSLATE_NoImageService, Language); + tlpnlGeneral.AdjustWidthToSize(1, _NO_TRANSLATE_cmbBranchesSortBy, _NO_TRANSLATE_cmbBranchesOrder, truncatePathMethod, _NO_TRANSLATE_NoImageService, Language); + tlpnlAuthor.AdjustWidthToSize(1, _NO_TRANSLATE_cmbBranchesSortBy, _NO_TRANSLATE_cmbBranchesOrder, truncatePathMethod, _NO_TRANSLATE_NoImageService, Language); + tlpnlLanguage.AdjustWidthToSize(1, _NO_TRANSLATE_cmbBranchesSortBy, _NO_TRANSLATE_cmbBranchesOrder, truncatePathMethod, _NO_TRANSLATE_NoImageService, Language); } public static SettingsPageReference GetPageReference() @@ -84,6 +87,8 @@ protected override void SettingsToPage() Language.Text = AppSettings.Translation; truncatePathMethod.SelectedIndex = GetTruncatePathMethodIndex(AppSettings.TruncatePathMethod); + _NO_TRANSLATE_cmbBranchesOrder.SelectedIndex = (int)AppSettings.RefsSortOrder; + _NO_TRANSLATE_cmbBranchesSortBy.SelectedIndex = (int)AppSettings.RefsSortBy; Dictionary.Items.Clear(); Dictionary.Items.Add(_noDictFile.Text); @@ -135,6 +140,8 @@ protected override void PageToSettings() AppSettings.ShowAuthorAvatarInCommitInfo = ShowAuthorAvatarInCommitInfo.Checked; AppSettings.AvatarImageCacheDays = (int)_NO_TRANSLATE_DaysToCacheImages.Value; AppSettings.SortByAuthorDate = chkSortByAuthorDate.Checked; + AppSettings.RefsSortOrder = (GitRefsSortOrder)_NO_TRANSLATE_cmbBranchesOrder.SelectedIndex; + AppSettings.RefsSortBy = (GitRefsSortBy)_NO_TRANSLATE_cmbBranchesSortBy.SelectedIndex; AppSettings.Translation = Language.Text; ResourceManager.Strings.Reinitialize(); @@ -162,20 +169,13 @@ protected override void PageToSettings() return; - TruncatePathMethod GetTruncatePathMethodString(int index) + TruncatePathMethod GetTruncatePathMethodString(int index) => index switch { - switch (index) - { - case 1: - return TruncatePathMethod.Compact; - case 2: - return TruncatePathMethod.TrimStart; - case 3: - return TruncatePathMethod.FileNameOnly; - default: - return TruncatePathMethod.None; - } - } + 1 => TruncatePathMethod.Compact, + 2 => TruncatePathMethod.TrimStart, + 3 => TruncatePathMethod.FileNameOnly, + _ => TruncatePathMethod.None, + }; } private void Dictionary_DropDown(object sender, EventArgs e) diff --git a/GitUI/Translation/English.xlf b/GitUI/Translation/English.xlf index f81baafe0c5..c93869f6cc1 100644 --- a/GitUI/Translation/English.xlf +++ b/GitUI/Translation/English.xlf @@ -192,6 +192,14 @@ See http://en.gravatar.com/site/implement/images#default-image for more details. Avatar provider + + Order branches + + + + Sort branches by + + Cache images (days) diff --git a/Plugins/GitUIPluginInterfaces/GitRefsOrder.cs b/Plugins/GitUIPluginInterfaces/GitRefsOrder.cs new file mode 100644 index 00000000000..e195fe3acfd --- /dev/null +++ b/Plugins/GitUIPluginInterfaces/GitRefsOrder.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; + +namespace GitUIPluginInterfaces +{ + // NB: The values are fed directly into git commands, casing is important! + public enum GitRefsSortBy + { + [Description("Git default")] + Default = 0, + + [Description("Author date")] + authordate, + + [Description("Committer date")] + committerdate, + + [Description("Creator date")] + creatordate, + + [Description("Tagger date")] + taggerdate, + + [Description("Alpha-numeric")] + refname, + + [Description("Object size")] + objectsize, + + [Description("Originating remote")] + upstream, + } + + public enum GitRefsSortOrder + { + [Description("A ↓ Z")] + Ascending = 0, + + [Description("Z ↑ A")] + Descending = 1, + } +} diff --git a/UnitTests/GitCommands.Tests/Git/Commands/GitCommandHelpersTest.cs b/UnitTests/GitCommands.Tests/Git/Commands/GitCommandHelpersTest.cs index 1627c277614..68a60921594 100644 --- a/UnitTests/GitCommands.Tests/Git/Commands/GitCommandHelpersTest.cs +++ b/UnitTests/GitCommands.Tests/Git/Commands/GitCommandHelpersTest.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.IO; using GitCommands; using GitCommands.Git; using GitCommands.Git.Commands; +using GitCommands.Utils; using GitUIPluginInterfaces; using NUnit.Framework; using ResourceManager; @@ -640,13 +642,45 @@ public void GetSortedRefsCommand(string expected, bool noLocks) Assert.AreEqual(expected, GitCommandHelpers.GetSortedRefsCommand(noLocks).ToString()); } - [TestCase(@"show-ref --dereference", true, true, false)] - [TestCase(@"show-ref --tags", true, false, false)] - [TestCase(@"for-each-ref --sort=-committerdate refs/heads/ --format=""%(objectname) %(refname)""", false, true, false)] - [TestCase(@"--no-optional-locks for-each-ref --sort=-committerdate refs/heads/ --format=""%(objectname) %(refname)""", false, true, true)] - public void GetRefsCmd(string expected, bool tags, bool branches, bool noLocks) + private static IEnumerable GetRefsCommandTestData { - Assert.AreEqual(expected, GitCommandHelpers.GetRefsCmd(tags, branches, noLocks).ToString()); + get + { + foreach (GitRefsSortBy sortBy in EnumHelper.GetValues()) + { + foreach (GitRefsSortOrder sortOrder in EnumHelper.GetValues()) + { + string sortCondition; + string sortConditionRef; + string format = @" --format=""%(if)%(authordate)%(then)%(objectname) %(refname)%(else)%(*objectname) %(*refname)%(end)"""; + if (sortBy == GitRefsSortBy.Default) + { + sortCondition = sortConditionRef = string.Empty; + } + else + { + string prefix = sortOrder == GitRefsSortOrder.Ascending ? string.Empty : "-"; + sortCondition = $" --sort={prefix}{sortBy}"; + sortConditionRef = $" --sort={prefix}*{sortBy}"; + } + + yield return new TestCaseData(/* tags */ true, /* branches */ true, /* noLocks */ false, sortBy, sortOrder, + /* expected */ $@"for-each-ref{sortCondition}{sortConditionRef}{format} refs/heads/ refs/remotes/ refs/tags/"); + yield return new TestCaseData(/* tags */ true, /* branches */ false, /* noLocks */ false, sortBy, sortOrder, + /* expected */ $@"for-each-ref{sortCondition}{sortConditionRef}{format} refs/tags/"); + yield return new TestCaseData(/* tags */ false, /* branches */ true, /* noLocks */ false, sortBy, sortOrder, + /* expected */ $@"for-each-ref{sortCondition} --format=""%(objectname) %(refname)"" refs/heads/"); + yield return new TestCaseData(/* tags */ false, /* branches */ true, /* noLocks */ true, sortBy, sortOrder, + /* expected */ $@"--no-optional-locks for-each-ref{sortCondition} --format=""%(objectname) %(refname)"" refs/heads/"); + } + } + } + } + + [TestCaseSource(nameof(GetRefsCommandTestData))] + public void GetRefsCmd(bool tags, bool branches, bool noLocks, GitRefsSortBy sortBy, GitRefsSortOrder sortOrder, string expected) + { + Assert.AreEqual(expected, GitCommandHelpers.GetRefsCmd(tags, branches, noLocks, sortBy, sortOrder).ToString()); } } } diff --git a/UnitTests/GitCommands.Tests/GitModuleTests.cs b/UnitTests/GitCommands.Tests/GitModuleTests.cs index dd19bfb4298..2c2737cbb8d 100644 --- a/UnitTests/GitCommands.Tests/GitModuleTests.cs +++ b/UnitTests/GitCommands.Tests/GitModuleTests.cs @@ -11,6 +11,7 @@ using GitCommands; using GitCommands.Git; using GitCommands.Git.Commands; +using GitCommands.Utils; using GitExtUtils; using GitUI; using GitUIPluginInterfaces; From a0f8eeae273f2b61f02690e9c165455b69e0285c Mon Sep 17 00:00:00 2001 From: RussKie Date: Sat, 29 Aug 2020 22:55:18 +1000 Subject: [PATCH 2/2] Expose sort by/order in the left panel --- GitCommands/IDiffListSortService.cs | 3 - .../GitRefsSortByContextMenuItem.cs | 73 ++++++++++++++ .../GitRefsSortOrderContextMenuItem.cs | 75 ++++++++++++++ .../RepoObjectsTree.ContextActions.cs | 55 ++++++++++- .../RepoObjectsTree.Nodes.Branches.cs | 36 ++++--- .../RepoObjectsTree.Nodes.Remotes.cs | 33 ++++--- .../RepoObjectsTree.Nodes.Tags.cs | 38 ++++--- .../BranchTreePanel/RepoObjectsTree.Nodes.cs | 2 +- GitUI/BranchTreePanel/RepoObjectsTree.cs | 17 +++- GitUI/Strings.cs | 6 ++ GitUI/Translation/English.xlf | 16 +++ GitUI/UserControls/FileStatusList.cs | 16 +-- .../SortDiffListContextMenuItem.cs | 14 +-- .../GitRefsSortByContextMenuItemTests.cs | 99 +++++++++++++++++++ .../GitRefsSortOrderContextMenuItemTests.cs | 99 +++++++++++++++++++ .../SortDiffListContextMenuItemTests.cs | 2 +- 16 files changed, 510 insertions(+), 74 deletions(-) create mode 100644 GitUI/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItem.cs create mode 100644 GitUI/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItem.cs create mode 100644 UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItemTests.cs create mode 100644 UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItemTests.cs diff --git a/GitCommands/IDiffListSortService.cs b/GitCommands/IDiffListSortService.cs index 163ce72032d..02b5ef2085e 100644 --- a/GitCommands/IDiffListSortService.cs +++ b/GitCommands/IDiffListSortService.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; namespace GitCommands { diff --git a/GitUI/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItem.cs b/GitUI/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItem.cs new file mode 100644 index 00000000000..9a140983ce5 --- /dev/null +++ b/GitUI/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItem.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using GitCommands; +using GitCommands.Utils; +using GitUI.Properties; +using GitUIPluginInterfaces; + +namespace GitUI.BranchTreePanel.ContextMenu +{ + internal class GitRefsSortByContextMenuItem : ToolStripMenuItem + { + private readonly Action _onSortByChanged; + + public GitRefsSortByContextMenuItem(Action onSortByChanged) + { + _onSortByChanged = onSortByChanged; + + Image = Images.SortBy; + Text = Strings.SortBy; + + foreach (var option in EnumHelper.GetValues().Select(e => (Text: e.GetDescription(), Value: e))) + { + var item = new ToolStripMenuItem() + { + Text = option.Text, + Image = null, + Tag = option.Value + }; + + item.Click += Item_Click; + DropDownItems.Add(item); + } + + DropDownOpening += (s, e) => RequerySortingMethod(); + RequerySortingMethod(); + } + + private void RequerySortingMethod() + { + var currentSort = AppSettings.RefsSortBy; + foreach (ToolStripMenuItem item in DropDownItems) + { + item.Checked = currentSort.Equals(item.Tag); + } + } + + private void Item_Click(object sender, EventArgs e) + { + if (sender is ToolStripMenuItem item) + { + var sortingType = (GitRefsSortBy)item.Tag; + AppSettings.RefsSortBy = sortingType; + + _onSortByChanged?.Invoke(); + } + } + + internal TestAccessor GetTestAccessor() => new TestAccessor(this); + + internal struct TestAccessor + { + private readonly GitRefsSortByContextMenuItem _contextMenuItem; + + public TestAccessor(GitRefsSortByContextMenuItem menuitem) + { + _contextMenuItem = menuitem; + } + + public void RaiseDropDownOpening() => _contextMenuItem.RequerySortingMethod(); + } + } +} diff --git a/GitUI/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItem.cs b/GitUI/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItem.cs new file mode 100644 index 00000000000..3d1a0a15bd2 --- /dev/null +++ b/GitUI/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItem.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using GitCommands; +using GitCommands.Utils; +using GitUI.Properties; +using GitUIPluginInterfaces; + +namespace GitUI.BranchTreePanel.ContextMenu +{ + internal class GitRefsSortOrderContextMenuItem : ToolStripMenuItem + { + internal const string MenuItemName = "GitRefsSortOrderContextMenuItem"; + private readonly Action _onSortOrderChanged; + + public GitRefsSortOrderContextMenuItem(Action onSortOrderChanged) + { + _onSortOrderChanged = onSortOrderChanged; + + Image = Images.SortBy; + Text = Strings.SortOrder; + Name = MenuItemName; + + foreach (var option in EnumHelper.GetValues().Select(e => (Text: e.GetDescription(), Value: e))) + { + var item = new ToolStripMenuItem() + { + Text = option.Text, + Image = null, + Tag = option.Value + }; + + item.Click += Item_Click; + DropDownItems.Add(item); + } + + DropDownOpening += (s, e) => RequerySortingMethod(); + RequerySortingMethod(); + } + + private void RequerySortingMethod() + { + var currentSort = AppSettings.RefsSortOrder; + foreach (ToolStripMenuItem item in DropDownItems) + { + item.Checked = currentSort.Equals(item.Tag); + } + } + + private void Item_Click(object sender, EventArgs e) + { + if (sender is ToolStripMenuItem item) + { + var sortingType = (GitRefsSortOrder)item.Tag; + AppSettings.RefsSortOrder = sortingType; + + _onSortOrderChanged?.Invoke(); + } + } + + internal TestAccessor GetTestAccessor() => new TestAccessor(this); + + internal struct TestAccessor + { + private readonly GitRefsSortOrderContextMenuItem _contextMenuItem; + + public TestAccessor(GitRefsSortOrderContextMenuItem menuitem) + { + _contextMenuItem = menuitem; + } + + public void RaiseDropDownOpening() => _contextMenuItem.RequerySortingMethod(); + } + } +} diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.ContextActions.cs b/GitUI/BranchTreePanel/RepoObjectsTree.ContextActions.cs index 5f96b3ba5bd..d3545cc1ad2 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.ContextActions.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.ContextActions.cs @@ -4,6 +4,7 @@ using System.Drawing; using System.Linq; using System.Windows.Forms; +using GitCommands; using GitUI.BranchTreePanel.ContextMenu; using GitUI.BranchTreePanel.Interfaces; using ResourceManager; @@ -13,6 +14,8 @@ namespace GitUI.BranchTreePanel partial class RepoObjectsTree : IMenuItemFactory { private TreeNode _lastRightClickedNode; + private GitRefsSortOrderContextMenuItem _sortOrderContextMenuItem; + private GitRefsSortByContextMenuItem _sortByContextMenuItem; /// /// Local branch context menu [git ref / rename / delete] actions @@ -104,7 +107,7 @@ private void ContextMenuBranchSpecific(ContextMenuStrip contextMenu) } var node = (contextMenu.SourceControl as TreeView)?.SelectedNode; - if (node == null) + if (node is null) { return; } @@ -121,7 +124,7 @@ private void ContextMenuRemoteRepoSpecific(ContextMenuStrip contextMenu) } var node = (contextMenu.SourceControl as TreeView)?.SelectedNode?.Tag as RemoteRepoNode; - if (node == null) + if (node is null) { return; } @@ -137,10 +140,38 @@ private void ContextMenuRemoteRepoSpecific(ContextMenuStrip contextMenu) mnubtnEnableRemoteAndFetch.Visible = !node.Enabled; } + private void ContextMenuSort(ContextMenuStrip contextMenu) + { + // We can only sort refs, i.e. branches and tags + if (contextMenu != menuBranch && + contextMenu != menuRemote && + contextMenu != menuTag) + { + return; + } + + // Add the following to the every participating context menu: + // + // --------- + // Sort By... + // Sort Order... + + if (!contextMenu.Items.Contains(_sortOrderContextMenuItem)) + { + AddContextMenuItems(contextMenu, + new ToolStripItem[] { _sortByContextMenuItem, _sortOrderContextMenuItem, new ToolStripSeparator() }, + tsmiMainMenuSpacer1); + } + + // If refs are sorted by git (GitRefsSortBy = Default) don't show sort order options + contextMenu.Items[GitRefsSortOrderContextMenuItem.MenuItemName].Visible = + AppSettings.RefsSortBy != GitUIPluginInterfaces.GitRefsSortBy.Default; + } + private void ContextMenuSubmoduleSpecific(ContextMenuStrip contextMenu) { TreeNode selectedNode = (contextMenu.SourceControl as TreeView)?.SelectedNode; - if (selectedNode == null) + if (selectedNode is null) { return; } @@ -184,6 +215,19 @@ private static void RegisterClick(ToolStripItem item, Action onClick) private void RegisterContextActions() { + _sortOrderContextMenuItem = new GitRefsSortOrderContextMenuItem(() => + { + _branchesTree.RefreshRefs(); + _remotesTree.RefreshRefs(); + _tagTree.RefreshRefs(); + }); + _sortByContextMenuItem = new GitRefsSortByContextMenuItem(() => + { + _branchesTree.RefreshRefs(); + _remotesTree.RefreshRefs(); + _tagTree.RefreshRefs(); + }); + _localBranchMenuItems = new LocalBranchMenuItems(this); AddContextMenuItems(menuBranch, _localBranchMenuItems.Select(s => s.Item)); @@ -247,12 +291,13 @@ private void FilterInRevisionGrid(BaseBranchNode branch) private void contextMenu_Opening(object sender, CancelEventArgs e) { var contextMenu = sender as ContextMenuStrip; - if (contextMenu == null) + if (contextMenu is null) { return; } ContextMenuAddExpandCollapseTree(contextMenu); + ContextMenuSort(contextMenu); ContextMenuBranchSpecific(contextMenu); ContextMenuRemoteRepoSpecific(contextMenu); ContextMenuSubmoduleSpecific(contextMenu); @@ -278,7 +323,7 @@ private void contextMenu_Opening(object sender, CancelEventArgs e) private void AddContextMenuItems(ContextMenuStrip menu, IEnumerable items, ToolStripItem insertAfter = null) { menu.SuspendLayout(); - int index = insertAfter == null ? 0 : Math.Max(0, menu.Items.IndexOf(insertAfter) + 1); + int index = insertAfter is null ? 0 : Math.Max(0, menu.Items.IndexOf(insertAfter) + 1); items.ForEach(item => menu.Items.Insert(index++, item)); menu.ResumeLayout(); } diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs index f56bb42ab72..1e96d4c8b0c 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Branches.cs @@ -8,6 +8,7 @@ using GitCommands.Git; using GitUI.BranchTreePanel.Interfaces; using GitUI.Properties; +using GitUIPluginInterfaces; using JetBrains.Annotations; using Microsoft.VisualStudio.Threading; @@ -118,9 +119,10 @@ private class BaseBranchLeafNode : BaseBranchNode private bool _isMerged = false; - public BaseBranchLeafNode(Tree tree, string fullPath, string imageKeyUnmerged, string imageKeyMerged) + public BaseBranchLeafNode(Tree tree, in ObjectId objectId, string fullPath, string imageKeyUnmerged, string imageKeyMerged) : base(tree, fullPath) { + ObjectId = objectId; _imageKeyUnmerged = imageKeyUnmerged; _imageKeyMerged = imageKeyMerged; } @@ -141,6 +143,9 @@ public bool IsMerged } } + [CanBeNull] + public ObjectId ObjectId { get; } + protected override void ApplyStyle() { base.ApplyStyle(); @@ -150,8 +155,8 @@ protected override void ApplyStyle() private sealed class LocalBranchNode : BaseBranchLeafNode, IGitRefActions, ICanRename, ICanDelete { - public LocalBranchNode(Tree tree, string fullPath, bool isCurrent) - : base(tree, fullPath, nameof(Images.BranchLocal), nameof(Images.BranchLocalMerged)) + public LocalBranchNode(Tree tree, in ObjectId objectId, string fullPath, bool isCurrent) + : base(tree, objectId, fullPath, nameof(Images.BranchLocal), nameof(Images.BranchLocalMerged)) { IsActive = isCurrent; } @@ -264,14 +269,19 @@ public BranchTree(TreeNode treeNode, IGitUICommandsSource uiCommands, [CanBeNull _aheadBehindDataProvider = aheadBehindDataProvider; } - protected override Task OnAttachedAsync() - { - return ReloadNodesAsync(LoadNodesAsync); - } + protected override Task OnAttachedAsync() => ReloadNodesAsync(LoadNodesAsync); + + protected override Task PostRepositoryChangedAsync() => ReloadNodesAsync(LoadNodesAsync); - protected override Task PostRepositoryChangedAsync() + /// + /// Requests to refresh the data tree retaining the current filtering rules. + /// + internal void RefreshRefs() { - return ReloadNodesAsync(LoadNodesAsync); + ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + await ReloadNodesAsync(LoadNodesAsync); + }); } private async Task LoadNodesAsync(CancellationToken token) @@ -279,11 +289,11 @@ private async Task LoadNodesAsync(CancellationToken token) await TaskScheduler.Default; token.ThrowIfCancellationRequested(); - var branchNames = Module.GetRefs(tags: false, branches: true).Select(b => b.Name); - return FillBranchTree(branchNames, token); + IReadOnlyList branches = Module.GetRefs(tags: false, branches: true); + return FillBranchTree(branches, token); } - private Nodes FillBranchTree(IEnumerable branches, CancellationToken token) + private Nodes FillBranchTree(IReadOnlyList branches, CancellationToken token) { #region ex @@ -323,7 +333,7 @@ private Nodes FillBranchTree(IEnumerable branches, CancellationToken tok foreach (var branch in branches) { token.ThrowIfCancellationRequested(); - var localBranchNode = new LocalBranchNode(this, branch, branch == currentBranch); + var localBranchNode = new LocalBranchNode(this, branch.ObjectId, branch.Name, branch.Name == currentBranch); if (aheadBehindData != null && aheadBehindData.ContainsKey(localBranchNode.FullPath)) { diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs index b926df06b76..8abc538df09 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Remotes.cs @@ -27,14 +27,19 @@ public RemoteBranchTree(TreeNode treeNode, IGitUICommandsSource uiCommands) { } - protected override Task OnAttachedAsync() - { - return ReloadNodesAsync(LoadNodesAsync); - } + protected override Task OnAttachedAsync() => ReloadNodesAsync(LoadNodesAsync); - protected override Task PostRepositoryChangedAsync() + protected override Task PostRepositoryChangedAsync() => ReloadNodesAsync(LoadNodesAsync); + + /// + /// Requests to refresh the data tree retaining the current filtering rules. + /// + internal void RefreshRefs() { - return ReloadNodesAsync(LoadNodesAsync); + ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + await ReloadNodesAsync(LoadNodesAsync); + }); } private async Task LoadNodesAsync(CancellationToken token) @@ -44,10 +49,8 @@ private async Task LoadNodesAsync(CancellationToken token) var nodes = new Nodes(this); var pathToNodes = new Dictionary(); - var branches = Module.GetRefs(tags: true, branches: true) - .Where(branch => branch.IsRemote && !branch.IsTag) - .OrderBy(branch => branch.Name) - .Select(branch => branch.Name); + IEnumerable branches = Module.GetRefs(tags: true, branches: true) + .Where(branch => branch.IsRemote && !branch.IsTag); token.ThrowIfCancellationRequested(); @@ -57,13 +60,13 @@ private async Task LoadNodesAsync(CancellationToken token) var remotesManager = new ConfigFileRemoteSettingsManager(() => Module); // Create nodes for enabled remotes with branches - foreach (var branchPath in branches) + foreach (IGitRef branch in branches) { token.ThrowIfCancellationRequested(); - var remoteName = branchPath.SubstringUntil('/'); + var remoteName = branch.Name.SubstringUntil('/'); if (remoteByName.TryGetValue(remoteName, out var remote)) { - var remoteBranchNode = new RemoteBranchNode(this, branchPath); + var remoteBranchNode = new RemoteBranchNode(this, branch.ObjectId, branch.Name); var parent = remoteBranchNode.CreateRootNode( pathToNodes, (tree, parentPath) => CreateRemoteBranchPathNode(tree, parentPath, remote)); @@ -157,8 +160,8 @@ internal bool FetchPruneAll() private sealed class RemoteBranchNode : BaseBranchLeafNode, IGitRefActions, ICanDelete, ICanRename { - public RemoteBranchNode(Tree tree, string fullPath) - : base(tree, fullPath, nameof(Images.BranchRemote), nameof(Images.BranchRemoteMerged)) + public RemoteBranchNode(Tree tree, in ObjectId objectId, string fullPath) + : base(tree, objectId, fullPath, nameof(Images.BranchRemote), nameof(Images.BranchRemoteMerged)) { } diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs index 55c3d6214d3..e29fc527d0e 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Tags.cs @@ -6,6 +6,7 @@ using GitUI.CommandsDialogs; using GitUI.Properties; using GitUIPluginInterfaces; +using JetBrains.Annotations; using Microsoft.VisualStudio.Threading; namespace GitUI.BranchTreePanel @@ -14,13 +15,15 @@ partial class RepoObjectsTree { private class TagNode : BaseBranchNode, IGitRefActions, ICanDelete { - private readonly IGitRef _tagInfo; - - public TagNode(Tree tree, string fullPath, IGitRef tagInfo) : base(tree, fullPath) + public TagNode(Tree tree, in ObjectId objectId, string fullPath) + : base(tree, fullPath) { - _tagInfo = tagInfo; + ObjectId = objectId; } + [CanBeNull] + public ObjectId ObjectId { get; } + internal override void OnSelected() { if (Tree.IgnoreSelectionChangedEvent) @@ -39,12 +42,12 @@ internal override void OnDoubleClick() public bool CreateBranch() { - return UICommands.StartCreateBranchDialog(TreeViewNode.TreeView, _tagInfo.ObjectId); + return UICommands.StartCreateBranchDialog(TreeViewNode.TreeView, ObjectId); } public bool Delete() { - return UICommands.StartDeleteTagDialog(TreeViewNode.TreeView, _tagInfo.Name); + return UICommands.StartDeleteTagDialog(TreeViewNode.TreeView, Name); } public bool Merge() @@ -75,14 +78,19 @@ public TagTree(TreeNode treeNode, IGitUICommandsSource uiCommands) { } - protected override Task OnAttachedAsync() - { - return ReloadNodesAsync(LoadNodesAsync); - } + protected override Task OnAttachedAsync() => ReloadNodesAsync(LoadNodesAsync); + + protected override Task PostRepositoryChangedAsync() => ReloadNodesAsync(LoadNodesAsync); - protected override Task PostRepositoryChangedAsync() + /// + /// Requests to refresh the data tree retaining the current filtering rules. + /// + internal void RefreshRefs() { - return ReloadNodesAsync(LoadNodesAsync); + ThreadHelper.JoinableTaskFactory.RunAsync(async () => + { + await ReloadNodesAsync(LoadNodesAsync); + }); } private async Task LoadNodesAsync(CancellationToken token) @@ -92,14 +100,14 @@ private async Task LoadNodesAsync(CancellationToken token) return FillTagTree(Module.GetRefs(tags: true, branches: false), token); } - private Nodes FillTagTree(IEnumerable tags, CancellationToken token) + private Nodes FillTagTree(IReadOnlyList tags, CancellationToken token) { var nodes = new Nodes(this); var pathToNodes = new Dictionary(); - foreach (var tag in tags) + foreach (IGitRef tag in tags) { token.ThrowIfCancellationRequested(); - var branchNode = new TagNode(this, tag.Name, tag); + var branchNode = new TagNode(this, tag.ObjectId, tag.Name); var parent = branchNode.CreateRootNode(pathToNodes, (tree, parentPath) => new BasePathNode(tree, parentPath)); if (parent != null) diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.cs index b47b1aec144..b54702e842b 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.cs @@ -406,7 +406,7 @@ public static Node GetNode(TreeNode treeNode) } [CanBeNull] - private static T GetNodeSafe([CanBeNull] TreeNode treeNode) where T : class, INode + internal static T GetNodeSafe([CanBeNull] TreeNode treeNode) where T : class, INode { return treeNode?.Tag as T; } diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.cs b/GitUI/BranchTreePanel/RepoObjectsTree.cs index 9a98c74ddc0..612d9775545 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.cs @@ -13,6 +13,7 @@ using GitUI.CommandsDialogs; using GitUI.Properties; using GitUI.UserControls; +using GitUIPluginInterfaces; using JetBrains.Annotations; using ResourceManager; @@ -224,7 +225,7 @@ bool IsOverride(MethodInfo m) } } - public void Initialize([CanBeNull]IAheadBehindDataProvider aheadBehindDataProvider, FilterBranchHelper filterBranchHelper) + public void Initialize([CanBeNull] IAheadBehindDataProvider aheadBehindDataProvider, FilterBranchHelper filterBranchHelper) { _aheadBehindDataProvider = aheadBehindDataProvider; _filterBranchHelper = filterBranchHelper; @@ -236,6 +237,13 @@ public void Initialize([CanBeNull]IAheadBehindDataProvider aheadBehindDataProvid public void SelectionChanged(IReadOnlyList selectedRevisions) { + // If we arrived here through the chain of events after selecting a node in the tree, + // and the selected revision is the one we have selected - do nothing. + if (selectedRevisions.Count == 1 && selectedRevisions[0].ObjectId == GetSelectedNodeObjectId(treeMain.SelectedNode)) + { + return; + } + var cancellationToken = _selectionCancellationTokenSequence.Next(); GitRevision selectedRevision = selectedRevisions.FirstOrDefault(); @@ -264,6 +272,13 @@ public void SelectionChanged(IReadOnlyList selectedRevisions) treeMain.EndUpdate(); } }).FileAndForget(); + + static ObjectId GetSelectedNodeObjectId(TreeNode treeNode) + { + // Local or remote branch nodes or tag nodes + return Node.GetNodeSafe(treeNode)?.ObjectId ?? + Node.GetNodeSafe(treeNode)?.ObjectId; + } } protected override void OnUICommandsSourceSet(IGitUICommandsSource source) diff --git a/GitUI/Strings.cs b/GitUI/Strings.cs index 7829a290f7c..27889c17840 100644 --- a/GitUI/Strings.cs +++ b/GitUI/Strings.cs @@ -61,6 +61,9 @@ internal sealed class Strings : Translate private readonly TranslationString _open = new TranslationString("Open"); private readonly TranslationString _directoryIsNotAValidRepository = new TranslationString("The selected item is not a valid git repository."); + private readonly TranslationString _sortBy = new TranslationString("&Sort by..."); + private readonly TranslationString _sortOrder = new TranslationString("&Sort order..."); + private readonly TranslationString _diffSelectedWithRememberedFile = new TranslationString("Diff with \"{0}\""); private readonly TranslationString _showDiffForAllParentsText = new TranslationString("Show file differences for all parents in browse dialog"); private readonly TranslationString _showDiffForAllParentsTooltip = new TranslationString(@"Show all differences between the selected commits, not limiting to only one difference. @@ -151,6 +154,9 @@ public static void Reinitialize() public static string Open => _instance.Value._open.Text; public static string DirectoryInvalidRepository => _instance.Value._directoryIsNotAValidRepository.Text; + public static string SortBy => _instance.Value._sortBy.Text; + public static string SortOrder => _instance.Value._sortOrder.Text; + public static string DiffSelectedWithRememberedFile => _instance.Value._diffSelectedWithRememberedFile.Text; public static string ShowDiffForAllParentsText => _instance.Value._showDiffForAllParentsText.Text; public static string ShowDiffForAllParentsTooltip => _instance.Value._showDiffForAllParentsTooltip.Text; diff --git a/GitUI/Translation/English.xlf b/GitUI/Translation/English.xlf index c93869f6cc1..4ac4b4e0f4b 100644 --- a/GitUI/Translation/English.xlf +++ b/GitUI/Translation/English.xlf @@ -7936,6 +7936,14 @@ To show all branches, right click the revision grid, select 'view' and then the Show/hide branches/remotes/tags + + &Sort by... + + + + &Sort order... + + Search: @@ -9416,6 +9424,14 @@ Select this commit to populate the full message. - For more than four selected commits, show the difference from the first to the last selected commit. + + &Sort by... + + + + &Sort order... + + Submodules diff --git a/GitUI/UserControls/FileStatusList.cs b/GitUI/UserControls/FileStatusList.cs index d72cb8ce588..6233b45f8f3 100644 --- a/GitUI/UserControls/FileStatusList.cs +++ b/GitUI/UserControls/FileStatusList.cs @@ -33,7 +33,6 @@ public sealed partial class FileStatusList : GitModuleControl private readonly TranslationString _diffBaseToB = new TranslationString("Unique diff BASE with b/"); private readonly TranslationString _diffCommonBase = new TranslationString("Common diff with BASE a/"); private readonly TranslationString _combinedDiff = new TranslationString("Combined diff"); - private readonly IGitRevisionTester _revisionTester; private readonly IFullPathResolver _fullPathResolver; private readonly SortDiffListContextMenuItem _sortByContextMenu; private readonly IReadOnlyList _noItemStatuses; @@ -77,7 +76,11 @@ public FileStatusList() InitializeComponent(); InitialiseFiltering(); CreateOpenSubmoduleMenuItem(); - _sortByContextMenu = CreateSortByContextMenuItem(); + _sortByContextMenu = new SortDiffListContextMenuItem(DiffListSortService.Instance) + { + Name = "sortListByContextMenuItem" + }; + SetupUnifiedDiffListSorting(); lblSplitter.Height = DpiUtil.Scale(1); InitializeComplete(); @@ -98,7 +101,6 @@ public FileStatusList() FilterComboBox.Font = new Font(FilterComboBox.Font, FontStyle.Bold); _fullPathResolver = new FullPathResolver(() => Module.WorkingDir); - _revisionTester = new GitRevisionTester(_fullPathResolver); _noItemStatuses = new[] { new GitItemStatus @@ -202,14 +204,6 @@ private void SetupUnifiedDiffListSorting() .Subscribe(); } - private static SortDiffListContextMenuItem CreateSortByContextMenuItem() - { - return new SortDiffListContextMenuItem(DiffListSortService.Instance) - { - Name = "sortListByContextMenuItem" - }; - } - // Properties [Browsable(false)] diff --git a/GitUI/UserControls/SortDiffListContextMenuItem.cs b/GitUI/UserControls/SortDiffListContextMenuItem.cs index 56df4d6eef3..73f15fabb62 100644 --- a/GitUI/UserControls/SortDiffListContextMenuItem.cs +++ b/GitUI/UserControls/SortDiffListContextMenuItem.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; using GitCommands; using GitUI.Properties; @@ -12,7 +9,6 @@ namespace GitUI.UserControls { public class SortDiffListContextMenuItem : ToolStripMenuItem { - private readonly TranslationString _sortByText = new TranslationString("&Sort by..."); private readonly TranslationString _filePathSortText = new TranslationString("File &Path"); private readonly TranslationString _fileExtensionSortText = new TranslationString("File &Extension"); private readonly TranslationString _fileStatusSortText = new TranslationString("File &Status"); @@ -26,7 +22,7 @@ public SortDiffListContextMenuItem(IDiffListSortService sortService) { _sortService = sortService ?? throw new ArgumentNullException(nameof(sortService)); Image = Images.SortBy; - Text = _sortByText.Text; + Text = Strings.SortBy; _filePathSortItem = new ToolStripMenuItem() { @@ -64,8 +60,6 @@ public SortDiffListContextMenuItem(IDiffListSortService sortService) RequerySortingMethod(); } - internal TestAccessor GetTestAccessor() => new TestAccessor(this); - private IReadOnlyList AllItems() { return _allItems; @@ -87,7 +81,9 @@ private void Item_Click(object sender, EventArgs e) _sortService.DiffListSorting = sortingType; } - public struct TestAccessor + internal TestAccessor GetTestAccessor() => new TestAccessor(this); + + internal struct TestAccessor { private readonly SortDiffListContextMenuItem _contextMenuItem; @@ -96,7 +92,7 @@ public TestAccessor(SortDiffListContextMenuItem menuitem) _contextMenuItem = menuitem; } - public void SimulateOpeningEvent() => _contextMenuItem.RequerySortingMethod(); + public void RaiseDropDownOpening() => _contextMenuItem.RequerySortingMethod(); } } } diff --git a/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItemTests.cs b/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItemTests.cs new file mode 100644 index 00000000000..548ade0f70a --- /dev/null +++ b/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortByContextMenuItemTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using GitCommands; +using GitCommands.Utils; +using GitUI.BranchTreePanel.ContextMenu; +using GitUIPluginInterfaces; +using NSubstitute; +using NUnit.Framework; + +namespace GitUITests.UserControls +{ + [SetCulture("en-US")] + [SetUICulture("en-US")] + [TestFixture] + public class GitRefsSortByContextMenuItemTests + { + private Action _onSortOrderChanged; + private GitRefsSortByContextMenuItem _itemUnderTest; + + [SetUp] + public void Setup() + { + _onSortOrderChanged = Substitute.For(); + _itemUnderTest = new GitRefsSortByContextMenuItem(_onSortOrderChanged); + } + + [Test] + public void Should_show_all_sort_options() + { + Assert.IsTrue(_itemUnderTest.HasDropDownItems); + Assert.AreEqual(EnumHelper.GetValues().Length, _itemUnderTest.DropDownItems.Count); + } + + private static IEnumerable SortOrderOptions + { + get + { + foreach (GitRefsSortBy order in EnumHelper.GetValues()) + { + yield return new TestCaseData(order); + } + } + } + + [TestCaseSource(nameof(SortOrderOptions))] + public void Only_the_current_sort_option_is_selected(GitRefsSortBy order) + { + GitRefsSortBy original = AppSettings.RefsSortBy; + try + { + AppSettings.RefsSortBy = order; + + // invoke the requery method to reselect the proper sub item + _itemUnderTest.GetTestAccessor().RaiseDropDownOpening(); + + AssertOnlyCheckedItemIs(order); + } + finally + { + AppSettings.RefsSortBy = original; + } + } + + [Test] + public void Clicking_an_item_sets_sort_in_service() + { + GitRefsSortBy original = AppSettings.RefsSortBy; + try + { + // Reset to the default + AppSettings.RefsSortBy = GitRefsSortBy.Default; + + foreach (var item in _itemUnderTest.DropDownItems.Cast()) + { + item.PerformClick(); + _onSortOrderChanged.Received(1).Invoke(); + _onSortOrderChanged.ClearReceivedCalls(); + } + } + finally + { + AppSettings.RefsSortBy = original; + } + } + + private void AssertOnlyCheckedItemIs(GitRefsSortBy sortType) + { + var matchingSubItem = _itemUnderTest.DropDownItems.Cast().Single(i => i.Tag.Equals(sortType)); + Assert.IsTrue(matchingSubItem.Checked); + + foreach (var otherItem in _itemUnderTest.DropDownItems.Cast().Except(new[] { matchingSubItem })) + { + Assert.IsFalse(otherItem.Checked); + } + } + } +} diff --git a/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItemTests.cs b/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItemTests.cs new file mode 100644 index 00000000000..4ce49b8014f --- /dev/null +++ b/UnitTests/GitUI.Tests/BranchTreePanel/ContextMenu/GitRefsSortOrderContextMenuItemTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using GitCommands; +using GitCommands.Utils; +using GitUI.BranchTreePanel.ContextMenu; +using GitUIPluginInterfaces; +using NSubstitute; +using NUnit.Framework; + +namespace GitUITests.UserControls +{ + [SetCulture("en-US")] + [SetUICulture("en-US")] + [TestFixture] + public class GitRefsSortOrderContextMenuItemTests + { + private Action _onSortOrderChanged; + private GitRefsSortOrderContextMenuItem _itemUnderTest; + + [SetUp] + public void Setup() + { + _onSortOrderChanged = Substitute.For(); + _itemUnderTest = new GitRefsSortOrderContextMenuItem(_onSortOrderChanged); + } + + [Test] + public void Should_show_all_sort_options() + { + Assert.IsTrue(_itemUnderTest.HasDropDownItems); + Assert.AreEqual(EnumHelper.GetValues().Length, _itemUnderTest.DropDownItems.Count); + } + + private static IEnumerable SortOrderOptions + { + get + { + foreach (GitRefsSortOrder order in EnumHelper.GetValues()) + { + yield return new TestCaseData(order); + } + } + } + + [TestCaseSource(nameof(SortOrderOptions))] + public void Only_the_current_sort_option_is_selected(GitRefsSortOrder order) + { + GitRefsSortOrder original = AppSettings.RefsSortOrder; + try + { + AppSettings.RefsSortOrder = order; + + // invoke the requery method to reselect the proper sub item + _itemUnderTest.GetTestAccessor().RaiseDropDownOpening(); + + AssertOnlyCheckedItemIs(order); + } + finally + { + AppSettings.RefsSortOrder = original; + } + } + + [Test] + public void Clicking_an_item_sets_sort_in_service() + { + GitRefsSortOrder original = AppSettings.RefsSortOrder; + try + { + // Reset to the default + AppSettings.RefsSortOrder = GitRefsSortOrder.Descending; + + foreach (var item in _itemUnderTest.DropDownItems.Cast()) + { + item.PerformClick(); + _onSortOrderChanged.Received(1).Invoke(); + _onSortOrderChanged.ClearReceivedCalls(); + } + } + finally + { + AppSettings.RefsSortOrder = original; + } + } + + private void AssertOnlyCheckedItemIs(GitRefsSortOrder sortType) + { + var matchingSubItem = _itemUnderTest.DropDownItems.Cast().Single(i => i.Tag.Equals(sortType)); + Assert.IsTrue(matchingSubItem.Checked); + + foreach (var otherItem in _itemUnderTest.DropDownItems.Cast().Except(new[] { matchingSubItem })) + { + Assert.IsFalse(otherItem.Checked); + } + } + } +} diff --git a/UnitTests/GitUI.Tests/UserControls/SortDiffListContextMenuItemTests.cs b/UnitTests/GitUI.Tests/UserControls/SortDiffListContextMenuItemTests.cs index 0b11284b53a..7a4c997ac86 100644 --- a/UnitTests/GitUI.Tests/UserControls/SortDiffListContextMenuItemTests.cs +++ b/UnitTests/GitUI.Tests/UserControls/SortDiffListContextMenuItemTests.cs @@ -50,7 +50,7 @@ public void Only_the_current_sort_option_is_selected(DiffListSortType sortType) _testingSortService.DiffListSorting.Returns(sortType); // invoke the requery method to reselect the proper sub item - _itemUnderTest.GetTestAccessor().SimulateOpeningEvent(); + _itemUnderTest.GetTestAccessor().RaiseDropDownOpening(); AssertOnlyCheckedItemIs(sortType); }