Skip to content

Commit

Permalink
GitStatus interactive react for gitextensions#5439
Browse files Browse the repository at this point in the history
Based on gitextensions#5429 no ignore
  • Loading branch information
gerhardol committed Sep 26, 2018
1 parent 64653e4 commit a292238
Showing 1 changed file with 85 additions and 67 deletions.
152 changes: 85 additions & 67 deletions GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using GitCommands;
using GitUIPluginInterfaces;
using JetBrains.Annotations;
using Microsoft.VisualStudio.Threading;
using CancellationToken = System.Threading.CancellationToken;

namespace GitUI.CommandsDialogs.BrowseDialog
{
Expand All @@ -16,7 +19,8 @@ public sealed class GitStatusMonitor : IDisposable
/// We often change several files at once.
/// Wait a second so they're all changed before we try to get the status.
/// </summary>
private const int UpdateDelay = 1000;
private const int InteractiveUpdateDelay = 200;
private const int FileChangedUpdateDelay = 1000;

/// <summary>
/// Minimum interval between subsequent updates
Expand All @@ -30,17 +34,14 @@ public sealed class GitStatusMonitor : IDisposable

private bool _commandIsRunning;
private bool _statusIsUpToDate = true;
private bool _ignoredFilesPending;

private readonly FileSystemWatcher _workTreeWatcher = new FileSystemWatcher();
private readonly FileSystemWatcher _gitDirWatcher = new FileSystemWatcher();
private readonly FileSystemWatcher _globalIgnoreWatcher = new FileSystemWatcher();

private readonly Timer _timerRefresh;
private readonly Timer _ignoredFilesTimer;

private string _globalIgnoreFilePath;
private bool _ignoredFilesAreStale;
private string _gitPath;
private string _submodulesPath;

Expand All @@ -49,7 +50,8 @@ public sealed class GitStatusMonitor : IDisposable
private int _previousUpdateTime;
private int _currentUpdateInterval = MinUpdateInterval;
private GitStatusMonitorState _currentStatus;
private HashSet<string> _ignoredFiles = new HashSet<string>();
private readonly CancellationTokenSequence _statusCancellation = new CancellationTokenSequence();
private CancellationToken _statusCancellationToken;

/// <summary>
/// Occurs whenever git status monitor state changes.
Expand All @@ -63,16 +65,14 @@ public sealed class GitStatusMonitor : IDisposable

public GitStatusMonitor(IGitUICommandsSource commandsSource)
{
_statusCancellationToken = _statusCancellation.Next();
_timerRefresh = new Timer
{
Enabled = true,
Interval = 500
};
_timerRefresh.Tick += delegate { Update(); };

_ignoredFilesTimer = new Timer { Interval = MaxUpdatePeriod };
_ignoredFilesTimer.Tick += delegate { _ignoredFilesAreStale = true; };

CurrentStatus = GitStatusMonitorState.Stopped;

// Setup a file watcher to detect changes to our files. When they
Expand Down Expand Up @@ -114,7 +114,7 @@ public GitStatusMonitor(IGitUICommandsSource commandsSource)
void WorkTreeWatcherError(object sender, ErrorEventArgs e)
{
// Called for instance at buffer overflow
CalculateNextUpdateTime(UpdateDelay);
CalculateNextUpdateTime();
}

void GitDirChanged(object sender, FileSystemEventArgs e)
Expand All @@ -137,20 +137,14 @@ void GitDirChanged(object sender, FileSystemEventArgs e)
return;
}

CalculateNextUpdateTime(UpdateDelay);
CalculateNextUpdateTime(isInterActive: true);
}

void WorkTreeChanged(object sender, FileSystemEventArgs e)
{
// Update already scheduled?
if (_nextUpdateTime < Environment.TickCount + UpdateDelay)
{
return;
}

var fileName = e.FullPath.Substring(_workTreeWatcher.Path.Length).ToPosixPath();
if (_ignoredFiles.Contains(fileName))
if (_nextUpdateTime <= Environment.TickCount + _currentUpdateInterval)
{
// Update sceduled already, no check needed
return;
}

Expand All @@ -172,25 +166,29 @@ void WorkTreeChanged(object sender, FileSystemEventArgs e)
return;
}

CalculateNextUpdateTime(UpdateDelay);
CalculateNextUpdateTime();
}

void GlobalIgnoreChanged(object sender, FileSystemEventArgs e)
{
if (e.FullPath == _globalIgnoreFilePath)
{
_ignoredFilesAreStale = true;
CalculateNextUpdateTime(UpdateDelay);
CalculateNextUpdateTime();
}
}
}

private bool IsAboutToUpdate()
{
return _nextUpdateTime < Environment.TickCount + InteractiveUpdateDelay && _nextUpdateTime >= Environment.TickCount;
}

public void Dispose()
{
_statusCancellation.Dispose();
_workTreeWatcher.Dispose();
_gitDirWatcher.Dispose();
_globalIgnoreWatcher.Dispose();
_ignoredFilesTimer.Dispose();
_timerRefresh.Dispose();
}

Expand Down Expand Up @@ -228,7 +226,7 @@ private GitStatusMonitorState CurrentStatus
_workTreeWatcher.EnableRaisingEvents = true;
_gitDirWatcher.EnableRaisingEvents = !_gitDirWatcher.Path.StartsWith(_workTreeWatcher.Path);
_globalIgnoreWatcher.EnableRaisingEvents = !string.IsNullOrWhiteSpace(_globalIgnoreWatcher.Path);
CalculateNextUpdateTime(UpdateDelay);
CalculateNextUpdateTime(isInterActive: true);
}

break;
Expand Down Expand Up @@ -263,7 +261,6 @@ void commandsSource_GitUICommandsChanged(object sender, GitUICommandsChangedEven
oldCommands.PreCheckoutRevision -= GitUICommands_PreCheckout;
oldCommands.PostCheckoutBranch -= GitUICommands_PostCheckout;
oldCommands.PostCheckoutRevision -= GitUICommands_PostCheckout;
oldCommands.PostEditGitIgnore -= GitUICommands_PostEditGitIgnore;
}

commandsSource_activate(sender as IGitUICommandsSource);
Expand All @@ -276,7 +273,6 @@ void commandsSource_activate(IGitUICommandsSource sender)
newCommands.PreCheckoutRevision += GitUICommands_PreCheckout;
newCommands.PostCheckoutBranch += GitUICommands_PostCheckout;
newCommands.PostCheckoutRevision += GitUICommands_PostCheckout;
newCommands.PostEditGitIgnore += GitUICommands_PostEditGitIgnore;

var module = newCommands.Module;
StartWatchingChanges(module.WorkingDir, module.WorkingDirGitDir);
Expand All @@ -291,16 +287,12 @@ void GitUICommands_PostCheckout(object sender, GitUIPostActionEventArgs e)
{
CurrentStatus = GitStatusMonitorState.Running;
}

void GitUICommands_PostEditGitIgnore(object sender, GitUIEventArgs e)
{
_ignoredFiles = new HashSet<string>();
_ignoredFilesAreStale = true;
}
}

private void StartWatchingChanges(string workTreePath, string gitDirPath)
{
_statusCancellationToken = _statusCancellation.Next();

// reset status info, it was outdated
GitWorkingDirectoryStatusChanged?.Invoke(this, new GitWorkingDirectoryStatusEventArgs());

Expand All @@ -322,10 +314,6 @@ private void StartWatchingChanges(string workTreePath, string gitDirPath)
_submodulesPath = Path.Combine(_gitPath, "modules");
_currentUpdateInterval = MinUpdateInterval;
_previousUpdateTime = 0;
_ignoredFilesAreStale = true;
_ignoredFiles = new HashSet<string>();
_ignoredFilesTimer.Stop();
_ignoredFilesTimer.Start();
CurrentStatus = GitStatusMonitorState.Running;
}
else
Expand Down Expand Up @@ -365,6 +353,8 @@ string DetermineGlobalIgnoreFilePath()

private void Update()
{
ThreadHelper.AssertOnUIThread();

if (CurrentStatus != GitStatusMonitorState.Running)
{
return;
Expand Down Expand Up @@ -394,71 +384,99 @@ private void Update()
_statusIsUpToDate = true;
_previousUpdateTime = Environment.TickCount;

// capture a consistent state in the main thread
var statusCancellationToken = _statusCancellationToken;
IGitModule module = Module;

ThreadHelper.JoinableTaskFactory.RunAsync(
async () =>
{
try
{
await TaskScheduler.Default;
_ignoredFilesPending = _ignoredFilesAreStale;
// git-status with ignored files when needed only
var cmd = GitCommandHelpers.GetAllChangedFilesCmd(!_ignoredFilesPending, UntrackedFilesMode.Default, noLocks: true);
var output = Module.RunGitCmd(cmd);
var changedFiles = GitCommandHelpers.GetStatusChangedFilesFromString(Module, output);
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var changedFiles = await GetChangedFilesAsync().ConfigureAwait(false);
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(statusCancellationToken);
UpdatedStatusReceived(changedFiles);
UpdateIgnoredCacheAsync(changedFiles).FileAndForget();
}
catch
finally
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
_commandIsRunning = false;
CurrentStatus = GitStatusMonitorState.Stopped;
// Schedule update every 5 min, even if we don't know that anything changed
CalculateNextUpdateTime(MaxUpdatePeriod);
}
})
.FileAndForget();

// Schedule update every 5 min, even if we don't know that anything changed
CalculateNextUpdateTime(MaxUpdatePeriod);

return;

void UpdatedStatusReceived(IReadOnlyList<GitItemStatus> changedFiles)
async Task<IEnumerable<GitItemStatus>> GetChangedFilesAsync()
{
try
{
await TaskScheduler.Default;
if (statusCancellationToken.IsCancellationRequested)
{
return Enumerable.Empty<GitItemStatus>();
}

var cmd = GitCommandHelpers.GetAllChangedFilesCmd(true, UntrackedFilesMode.Default, noLocks: true);
var output = module.RunGitCmd(cmd);
return GitCommandHelpers.GetStatusChangedFilesFromString(module, output);
}
catch
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(statusCancellationToken);
CurrentStatus = GitStatusMonitorState.Stopped;

throw;
}
}

async Task UpdateIgnoredCacheAsync(IEnumerable<GitItemStatus> changedFiles)
{
await TaskScheduler.Default;
if (statusCancellationToken.IsCancellationRequested)
{
return;
}

if (IsAboutToUpdate())
{
return;
}
}

void UpdatedStatusReceived(IEnumerable<GitItemStatus> changedFiles)
{
// Adjust the interval between updates. (This does not affect an update already scheduled).
_currentUpdateInterval = Math.Max(MinUpdateInterval, 3 * (Environment.TickCount - _previousUpdateTime));
_commandIsRunning = false;

if (CurrentStatus != GitStatusMonitorState.Running)
{
return;
}

GitWorkingDirectoryStatusChanged?.Invoke(this, new GitWorkingDirectoryStatusEventArgs(changedFiles.Where(item => !item.IsIgnored)));
if (_ignoredFilesPending)
{
_ignoredFilesPending = false;
_ignoredFiles = new HashSet<string>(changedFiles.Where(item => item.IsIgnored).Select(item => item.Name));
if (_statusIsUpToDate)
{
_ignoredFilesAreStale = false;
}
}
GitWorkingDirectoryStatusChanged?.Invoke(this, new GitWorkingDirectoryStatusEventArgs(changedFiles));

if (!_statusIsUpToDate)
{
// Still not up-to-date, but present what received, GetAllChangedFilesCmd() is the heavy command
CalculateNextUpdateTime(UpdateDelay);
CalculateNextUpdateTime();
}
}
}

private void CalculateNextUpdateTime(int delay)
private void CalculateNextUpdateTime(int delay = FileChangedUpdateDelay, bool isInterActive = false)
{
int currentUpdateInterval = _currentUpdateInterval;
if (isInterActive)
{
delay = InteractiveUpdateDelay;
currentUpdateInterval = InteractiveUpdateDelay;
}

var next = Environment.TickCount + delay;
if (_nextUpdateTime > Environment.TickCount)
{
Expand All @@ -467,7 +485,7 @@ private void CalculateNextUpdateTime(int delay)
}

// Enforce a minimal time between updates, to not update too frequently
_nextUpdateTime = Math.Max(next, _previousUpdateTime + _currentUpdateInterval);
_nextUpdateTime = Math.Max(next, _previousUpdateTime + currentUpdateInterval);
}
}
}

0 comments on commit a292238

Please sign in to comment.