Skip to content

Commit

Permalink
Submodule: Recreate tree only at structure changes 3.5 (#8978)
Browse files Browse the repository at this point in the history
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 ca7e05c)

Co-authored-by: Gerhard Olsson <gerhardol@users.noreply.github.com>
  • Loading branch information
gerhardol and gerhardol committed Mar 12, 2021
1 parent 597f876 commit 750ee24
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 56 deletions.
8 changes: 7 additions & 1 deletion GitCommands/Submodules/SubmoduleStatusEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ public class SubmoduleStatusEventArgs : EventArgs
{
public SubmoduleInfoResult Info { get; }

/// <summary>
/// First update of the submodule structure. Status of the submodule will be updated asynchronously.
/// </summary>
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;
}
}
}
10 changes: 5 additions & 5 deletions GitCommands/Submodules/SubmoduleStatusProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down Expand Up @@ -165,18 +165,18 @@ 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()
{
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));
}

/// <summary>
Expand Down
81 changes: 63 additions & 18 deletions GitUI/BranchTreePanel/RepoObjectsTree.Nodes.Submodules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GitItemStatus> GitStatus { get; }
public string LocalPath { get; }
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
Expand All @@ -256,23 +259,65 @@ private void OnStatusUpdated(SubmoduleStatusEventArgs e)
{
CancellationTokenSource cts = null;
Task<Nodes> 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<SubmoduleNode>().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<Nodes> LoadNodesAsync(SubmoduleInfoResult info, CancellationToken token)
{
Expand Down
91 changes: 59 additions & 32 deletions GitUI/CommandsDialogs/FormBrowse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ToolStripItem> _currentSubmoduleMenuItems;
private readonly FormBrowseDiagnosticsReporter _formBrowseDiagnosticsReporter;
[CanBeNull] private BuildReportTabPageExtension _buildReportTabPageExtension;
private readonly ShellProvider _shellProvider = new();
Expand Down Expand Up @@ -2730,8 +2731,7 @@ private void toolStripButtonLevelUp_DropDownOpening(object sender, EventArgs e)

#region Submodules

private (ToolStripItem item, Func<Task<Action>> 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))
{
Expand All @@ -2747,14 +2747,18 @@ private void toolStripButtonLevelUp_DropDownOpening(object sender, EventArgs e)

item.Click += SubmoduleToolStripButtonClick;

Func<Task<Action>> 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)
{
Expand Down Expand Up @@ -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<List<ToolStripItem>> 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<Action>();
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()
Expand Down

0 comments on commit 750ee24

Please sign in to comment.