From 750ee2412bf41d6b7d5f43dd5e1c93f878170ded Mon Sep 17 00:00:00 2001 From: Gerhard Olsson Date: Fri, 12 Mar 2021 13:56:24 +0100 Subject: [PATCH] Submodule: Recreate tree only at structure changes 3.5 (#8978) This applies both for the sidepanel and browse toolbar menu. The submodule provider stsus updates always contains the full trees, but as the provider notifies when a new list is being created, sidepanel/toolbar-menu can know when a new structure must be created. * Browse submodule menu: Refactor Remove obsolete submodule detail handling SubmoduleStatusProvider calculates the required info. (cherry picked from commit ca7e05c421c7c3b80eb97dd2a785be543ceb92bf) Co-authored-by: Gerhard Olsson --- .../Submodules/SubmoduleStatusEventArgs.cs | 8 +- .../Submodules/SubmoduleStatusProvider.cs | 10 +- .../RepoObjectsTree.Nodes.Submodules.cs | 81 +++++++++++++---- GitUI/CommandsDialogs/FormBrowse.cs | 91 ++++++++++++------- 4 files changed, 134 insertions(+), 56 deletions(-) diff --git a/GitCommands/Submodules/SubmoduleStatusEventArgs.cs b/GitCommands/Submodules/SubmoduleStatusEventArgs.cs index 031ab52b385..82a1bc4c17b 100644 --- a/GitCommands/Submodules/SubmoduleStatusEventArgs.cs +++ b/GitCommands/Submodules/SubmoduleStatusEventArgs.cs @@ -7,12 +7,18 @@ public class SubmoduleStatusEventArgs : EventArgs { public SubmoduleInfoResult Info { get; } + /// + /// First update of the submodule structure. Status of the submodule will be updated asynchronously. + /// + public bool StructureUpdated { get; } + public CancellationToken Token { get; } - public SubmoduleStatusEventArgs(SubmoduleInfoResult info, CancellationToken token) + public SubmoduleStatusEventArgs(SubmoduleInfoResult info, bool structureUpdated, CancellationToken token) { Info = info; Token = token; + StructureUpdated = structureUpdated; } } } diff --git a/GitCommands/Submodules/SubmoduleStatusProvider.cs b/GitCommands/Submodules/SubmoduleStatusProvider.cs index dbd023dd75c..28b904fe30d 100644 --- a/GitCommands/Submodules/SubmoduleStatusProvider.cs +++ b/GitCommands/Submodules/SubmoduleStatusProvider.cs @@ -100,7 +100,7 @@ public async Task UpdateSubmodulesStructureAsync(string workingDirectory, string } // Structure is updated - OnStatusUpdated(result, cancelToken); + OnStatusUpdated(result, structureUpdated: true, cancelToken); if (updateStatus && currentModule.SuperprojectModule is not null) { @@ -127,7 +127,7 @@ public async Task UpdateSubmodulesStructureAsync(string workingDirectory, string // Ignore if possible or at least delay the pending git-status trigger _previousSubmoduleUpdateTime = DateTime.Now; _submodulesStatusSequence.Next(); - OnStatusUpdated(result, cancelToken); + OnStatusUpdated(result, structureUpdated: false, cancelToken); } _submoduleInfoResult = result; @@ -165,7 +165,7 @@ public async Task UpdateSubmodulesStatusAsync(string workingDirectory, IReadOnly var currentModule = new GitModule(workingDirectory); await UpdateSubmodulesStatusAsync(currentModule, gitStatus, cancelToken); - OnStatusUpdated(_submoduleInfoResult, cancelToken); + OnStatusUpdated(_submoduleInfoResult, structureUpdated: false, cancelToken); } private void OnStatusUpdating() @@ -173,10 +173,10 @@ private void OnStatusUpdating() StatusUpdating?.Invoke(this, EventArgs.Empty); } - private void OnStatusUpdated(SubmoduleInfoResult info, CancellationToken token) + private void OnStatusUpdated(SubmoduleInfoResult info, bool structureUpdated, CancellationToken token) { token.ThrowIfCancellationRequested(); - StatusUpdated?.Invoke(this, new SubmoduleStatusEventArgs(info, token)); + StatusUpdated?.Invoke(this, new SubmoduleStatusEventArgs(info, structureUpdated, token)); } /// diff --git a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Submodules.cs b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Submodules.cs index 696ca1c7fb3..73ded914fe8 100644 --- a/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Submodules.cs +++ b/GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Submodules.cs @@ -47,7 +47,7 @@ protected override void ApplyStyle() // Node representing a submodule private class SubmoduleNode : Node { - public SubmoduleInfo Info { get; } + public SubmoduleInfo Info { get; set; } public bool IsCurrent { get; } public IReadOnlyList GitStatus { get; } public string LocalPath { get; } @@ -75,10 +75,7 @@ public SubmoduleNode(Tree tree, SubmoduleInfo submoduleInfo, bool isCurrent, IRe public void RefreshDetails() { - if (Info.Detailed is not null && Tree.TreeViewNode.TreeView is not null) - { - ApplyStatus(); - } + ApplyStatus(); } public bool CanOpen => !IsCurrent; @@ -135,15 +132,14 @@ protected override void ApplyStyle() TreeViewNode.NodeFont = new Font(AppSettings.Font, FontStyle.Bold); } - TreeViewNode.ToolTipText = DisplayText(); - // Note that status is applied also after the tree is created, when status is applied ApplyStatus(); } private void ApplyStatus() { - TreeViewNode.ImageKey = GetSubmoduleItemImage(Info.Detailed); + TreeViewNode.ToolTipText = DisplayText(); + TreeViewNode.ImageKey = GetSubmoduleItemImage(Info?.Detailed); TreeViewNode.SelectedImageKey = TreeViewNode.ImageKey; return; @@ -222,10 +218,12 @@ public DummyNode() : base(null) private sealed class SubmoduleTree : Tree { private SubmoduleStatusEventArgs _currentSubmoduleInfo; + private Nodes _currentNodes = null; public SubmoduleTree(TreeNode treeNode, IGitUICommandsSource uiCommands) : base(treeNode, uiCommands) { + SubmoduleStatusProvider.Default.StatusUpdating += Provider_StatusUpdating; SubmoduleStatusProvider.Default.StatusUpdated += Provider_StatusUpdated; } @@ -240,6 +238,11 @@ protected override Task OnAttachedAsync() return Task.CompletedTask; } + private void Provider_StatusUpdating(object sender, EventArgs e) + { + _currentNodes = null; + } + private void Provider_StatusUpdated(object sender, SubmoduleStatusEventArgs e) { _currentSubmoduleInfo = e; @@ -256,23 +259,65 @@ private void OnStatusUpdated(SubmoduleStatusEventArgs e) { CancellationTokenSource cts = null; Task loadNodesTask = null; - await ReloadNodesAsync(token => + if (e.StructureUpdated) { - cts = CancellationTokenSource.CreateLinkedTokenSource(e.Token, token); - loadNodesTask = LoadNodesAsync(e.Info, cts.Token); - return loadNodesTask; - }).ConfigureAwait(false); + _currentNodes = null; + } + + if (_currentNodes is not null) + { + // Structure is up-to-date, update status + var infos = e.Info.AllSubmodules.ToDictionary(info => info.Path, info => info); + infos[e.Info.TopProject.Path] = e.Info.TopProject; + var nodes = _currentNodes.DepthEnumerator().ToList(); + foreach (var node in nodes) + { + if (infos.ContainsKey(node.Info.Path)) + { + node.Info = infos[node.Info.Path]; + infos.Remove(node.Info.Path); + } + else + { + // structure no longer matching + Debug.Assert(true, $"Status info with {1 + e.Info.AllSubmodules.Count} records do not match current nodes ({nodes.Count})"); + _currentNodes = null; + break; + } + } + + if (infos.Count > 0) + { + Debug.Fail($"{infos.Count} status info records remains after matching current nodes, structure seem to mismatch ({nodes.Count}/{e.Info.AllSubmodules.Count})"); + _currentNodes = null; + } + } + + if (_currentNodes is null) + { + await ReloadNodesAsync(token => + { + cts = CancellationTokenSource.CreateLinkedTokenSource(e.Token, token); + loadNodesTask = LoadNodesAsync(e.Info, cts.Token); + return loadNodesTask; + }).ConfigureAwait(false); + } if (cts is not null && loadNodesTask is not null) { - var loadedNodes = await loadNodesTask; - await LoadNodeDetailsAsync(loadedNodes, cts.Token).ConfigureAwaitRunInline(); - LoadNodeToolTips(loadedNodes, cts.Token); + _currentNodes = await loadNodesTask; + } + + if (_currentNodes is not null) + { + var token = cts?.Token ?? e.Token; + await LoadNodeDetailsAsync(_currentNodes, token).ConfigureAwaitRunInline(); + LoadNodeToolTips(_currentNodes, token); } Interlocked.CompareExchange(ref _currentSubmoduleInfo, null, e); - }).FileAndForget(); - } + }).FileAndForget(); + } private async Task LoadNodesAsync(SubmoduleInfoResult info, CancellationToken token) { diff --git a/GitUI/CommandsDialogs/FormBrowse.cs b/GitUI/CommandsDialogs/FormBrowse.cs index 27b1b60dbe0..482abf81b3b 100644 --- a/GitUI/CommandsDialogs/FormBrowse.cs +++ b/GitUI/CommandsDialogs/FormBrowse.cs @@ -95,6 +95,7 @@ public sealed partial class FormBrowse : GitModuleForm, IBrowseRepo [CanBeNull] private readonly IAheadBehindDataProvider _aheadBehindDataProvider; private readonly WindowsJumpListManager _windowsJumpListManager; private readonly ISubmoduleStatusProvider _submoduleStatusProvider; + private List _currentSubmoduleMenuItems; private readonly FormBrowseDiagnosticsReporter _formBrowseDiagnosticsReporter; [CanBeNull] private BuildReportTabPageExtension _buildReportTabPageExtension; private readonly ShellProvider _shellProvider = new(); @@ -2730,8 +2731,7 @@ private void toolStripButtonLevelUp_DropDownOpening(object sender, EventArgs e) #region Submodules - private (ToolStripItem item, Func> loadDetails) - CreateSubmoduleMenuItem(CancellationToken cancelToken, SubmoduleInfo info, string textFormat = "{0}") + private ToolStripItem CreateSubmoduleMenuItem(SubmoduleInfo info, string textFormat = "{0}") { var item = new ToolStripMenuItem(string.Format(textFormat, info.Text)) { @@ -2747,14 +2747,18 @@ private void toolStripButtonLevelUp_DropDownOpening(object sender, EventArgs e) item.Click += SubmoduleToolStripButtonClick; - Func> loadDetails = null; + return item; + } + + private void UpdateSubmoduleMenuItemStatus(ToolStripItem item, SubmoduleInfo info, string textFormat = "{0}") + { if (info.Detailed is not null) { item.Image = GetSubmoduleItemImage(info.Detailed); item.Text = string.Format(textFormat, info.Text + info.Detailed.AddedAndRemovedText); } - return (item, loadDetails); + return; Image GetSubmoduleItemImage(DetailedSubmoduleInfo details) { @@ -2823,78 +2827,101 @@ private void SubmoduleStatusProvider_StatusUpdated(object sender, SubmoduleStatu { ThreadHelper.JoinableTaskFactory.RunAsync(async () => { - await PopulateToolbarAsync(e.Info, e.Token); + if (e.StructureUpdated || _currentSubmoduleMenuItems is null) + { + _currentSubmoduleMenuItems = await PopulateToolbarAsync(e.Info, e.Token); + } + + await UpdateSubmoduleMenuStatusAsync(e.Info, e.Token); }).FileAndForget(); } - private async Task PopulateToolbarAsync(SubmoduleInfoResult result, CancellationToken cancelToken) + private async Task> PopulateToolbarAsync(SubmoduleInfoResult result, CancellationToken cancelToken) { - // Second task: Populate toolbar menu on UI thread. Note further tasks are created by - // CreateSubmoduleMenuItem to update images with submodule status. + // Second task: Populate submodule toolbar menu on UI thread. await this.SwitchToMainThreadAsync(cancelToken); RemoveSubmoduleButtons(); var newItems = result.OurSubmodules - .Select(submodule => CreateSubmoduleMenuItem(cancelToken, submodule)) + .Select(submodule => CreateSubmoduleMenuItem(submodule)) .ToList(); if (result.OurSubmodules.Count == 0) { - newItems.Add((new ToolStripMenuItem(_noSubmodulesPresent.Text), null)); + newItems.Add(new ToolStripMenuItem(_noSubmodulesPresent.Text)); } if (result.SuperProject is not null) { - newItems.Add((new ToolStripSeparator(), null)); + newItems.Add(new ToolStripSeparator()); // Show top project only if it's not our super project if (result.TopProject is not null && result.TopProject != result.SuperProject) { - newItems.Add(CreateSubmoduleMenuItem(cancelToken, result.TopProject, _topProjectModuleFormat.Text)); + newItems.Add(CreateSubmoduleMenuItem(result.TopProject, _topProjectModuleFormat.Text)); } - newItems.Add(CreateSubmoduleMenuItem(cancelToken, result.SuperProject, _superprojectModuleFormat.Text)); - newItems.AddRange(result.AllSubmodules.Select(submodule => CreateSubmoduleMenuItem(cancelToken, submodule))); + newItems.Add(CreateSubmoduleMenuItem(result.SuperProject, _superprojectModuleFormat.Text)); + newItems.AddRange(result.AllSubmodules.Select(submodule => CreateSubmoduleMenuItem(submodule))); toolStripButtonLevelUp.ToolTipText = _goToSuperProject.Text; } - newItems.Add((new ToolStripSeparator(), null)); + newItems.Add(new ToolStripSeparator()); var mi = new ToolStripMenuItem(updateAllSubmodulesToolStripMenuItem.Text, Images.SubmodulesUpdate); mi.Click += UpdateAllSubmodulesToolStripMenuItemClick; - newItems.Add((mi, null)); + newItems.Add(mi); if (result.CurrentSubmoduleName is not null) { - var item = new ToolStripMenuItem(_updateCurrentSubmodule.Text) { Tag = result.CurrentSubmoduleName }; + var item = new ToolStripMenuItem(_updateCurrentSubmodule.Text) + { + Width = 200, + Tag = Module.WorkingDir, + Image = Images.FolderSubmodule + }; item.Click += UpdateSubmoduleToolStripMenuItemClick; - newItems.Add((item, null)); + newItems.Add(item); } // Using AddRange is critical: if you used Add to add menu items one at a // time, performance would be extremely slow with many submodules (> 100). - toolStripButtonLevelUp.DropDownItems.AddRange(newItems.Select(e => e.item).ToArray()); + toolStripButtonLevelUp.DropDownItems.AddRange(newItems.ToArray()); - // Load details sequentially to not spawn too many background threads - // then refresh all items at once with a single switch to the main thread - ThreadHelper.JoinableTaskFactory.RunAsync(async () => + return newItems; + } + + private async Task UpdateSubmoduleMenuStatusAsync(SubmoduleInfoResult result, CancellationToken cancelToken) + { + if (_currentSubmoduleMenuItems is null) { - var loadDetails = newItems.Select(e => e.loadDetails).Where(e => e is not null); - var refreshActions = new List(); - foreach (var loadFunc in loadDetails) + return; + } + + await this.SwitchToMainThreadAsync(cancelToken); + + var infos = result.AllSubmodules.ToDictionary(info => info.Path, info => info); + infos[result.TopProject.Path] = result.TopProject; + foreach (var item in _currentSubmoduleMenuItems) + { + var path = item.Tag as string; + if (string.IsNullOrWhiteSpace(path)) { - cancelToken.ThrowIfCancellationRequested(); - var action = await loadFunc(); - refreshActions.Add(action); + // not a submodule + continue; } - await this.SwitchToMainThreadAsync(cancelToken); - foreach (var refreshAction in refreshActions) + if (infos.ContainsKey(path)) { - refreshAction(); + UpdateSubmoduleMenuItemStatus(item, infos[path]); } - }).FileAndForget(); + else + { + Debug.Fail($"Status info for {path} ({1 + result.AllSubmodules.Count} records) has no match in current nodes ({_currentSubmoduleMenuItems.Count})"); + break; + } + } } private void RemoveSubmoduleButtons()