diff --git a/GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs b/GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs
index b9775f6a22f..15a8a31cc4f 100644
--- a/GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs
+++ b/GitUI/CommandsDialogs/BrowseDialog/GitStatusMonitor.cs
@@ -7,16 +7,23 @@
using GitCommands;
using GitUIPluginInterfaces;
using Microsoft.VisualStudio.Threading;
+using CancellationToken = System.Threading.CancellationToken;
namespace GitUI.CommandsDialogs.BrowseDialog
{
public sealed class GitStatusMonitor : IDisposable
{
+ ///
+ /// We often change several files at once.
+ /// Short delay before we try to get the status.
+ ///
+ private const int InteractiveUpdateDelay = 200;
+
///
/// We often change several files at once.
/// Wait a second so they're all changed before we try to get the status.
///
- private const int UpdateDelay = 1000;
+ private const int FileChangedUpdateDelay = 1000;
///
/// Minimum interval between subsequent updates
@@ -26,21 +33,16 @@ public sealed class GitStatusMonitor : IDisposable
///
/// Update every 5min, just to make sure something didn't slip through the cracks.
///
- private const int MaxUpdatePeriod = 5 * 60 * 1000;
+ private const int PeriodicUpdateInterval = 5 * 60 * 1000;
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;
@@ -49,7 +51,8 @@ public sealed class GitStatusMonitor : IDisposable
private int _previousUpdateTime;
private int _currentUpdateInterval = MinUpdateInterval;
private GitStatusMonitorState _currentStatus;
- private HashSet _ignoredFiles = new HashSet();
+ private readonly CancellationTokenSequence _statusCancellation = new CancellationTokenSequence();
+ private CancellationToken _statusCancellationToken;
///
/// Occurs whenever git status monitor state changes.
@@ -63,6 +66,7 @@ public sealed class GitStatusMonitor : IDisposable
public GitStatusMonitor(IGitUICommandsSource commandsSource)
{
+ _statusCancellationToken = _statusCancellation.Next();
_timerRefresh = new Timer
{
Enabled = true,
@@ -70,9 +74,6 @@ public GitStatusMonitor(IGitUICommandsSource commandsSource)
};
_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
@@ -96,17 +97,6 @@ public GitStatusMonitor(IGitUICommandsSource commandsSource)
_gitDirWatcher.IncludeSubdirectories = true;
_gitDirWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
- // Setup a file watcher to detect changes to the global ignore file. When it
- // changes, we'll update our status.
- _globalIgnoreWatcher.EnableRaisingEvents = false;
- _globalIgnoreWatcher.Changed += GlobalIgnoreChanged;
- _globalIgnoreWatcher.Created += GlobalIgnoreChanged;
- _globalIgnoreWatcher.Deleted += GlobalIgnoreChanged;
- _globalIgnoreWatcher.Renamed += GlobalIgnoreChanged;
- _globalIgnoreWatcher.Error += WorkTreeWatcherError;
- _globalIgnoreWatcher.IncludeSubdirectories = false;
- _globalIgnoreWatcher.NotifyFilter = NotifyFilters.LastWrite;
-
Init(commandsSource);
return;
@@ -114,7 +104,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)
@@ -137,20 +127,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 (IsAboutToUpdate())
{
+ // Update scheduled already, no check needed
return;
}
@@ -172,30 +156,25 @@ void WorkTreeChanged(object sender, FileSystemEventArgs e)
return;
}
- CalculateNextUpdateTime(UpdateDelay);
+ CalculateNextUpdateTime();
}
+ }
- void GlobalIgnoreChanged(object sender, FileSystemEventArgs e)
- {
- if (e.FullPath == _globalIgnoreFilePath)
- {
- _ignoredFilesAreStale = true;
- CalculateNextUpdateTime(UpdateDelay);
- }
- }
+ private bool IsAboutToUpdate()
+ {
+ return _nextUpdateTime <= Environment.TickCount + _currentUpdateInterval;
}
public void RequestRefresh()
{
- CalculateNextUpdateTime(UpdateDelay);
+ CalculateNextUpdateTime(isInteractive: true);
}
public void Dispose()
{
+ _statusCancellation.Dispose();
_workTreeWatcher.Dispose();
_gitDirWatcher.Dispose();
- _globalIgnoreWatcher.Dispose();
- _ignoredFilesTimer.Dispose();
_timerRefresh.Dispose();
}
@@ -212,7 +191,6 @@ private GitStatusMonitorState CurrentStatus
_timerRefresh.Stop();
_workTreeWatcher.EnableRaisingEvents = false;
_gitDirWatcher.EnableRaisingEvents = false;
- _globalIgnoreWatcher.EnableRaisingEvents = false;
}
break;
@@ -222,7 +200,6 @@ private GitStatusMonitorState CurrentStatus
_timerRefresh.Stop();
_workTreeWatcher.EnableRaisingEvents = false;
_gitDirWatcher.EnableRaisingEvents = false;
- _globalIgnoreWatcher.EnableRaisingEvents = false;
}
break;
@@ -232,8 +209,7 @@ private GitStatusMonitorState CurrentStatus
_timerRefresh.Start();
_workTreeWatcher.EnableRaisingEvents = true;
_gitDirWatcher.EnableRaisingEvents = !_gitDirWatcher.Path.StartsWith(_workTreeWatcher.Path);
- _globalIgnoreWatcher.EnableRaisingEvents = !string.IsNullOrWhiteSpace(_globalIgnoreWatcher.Path);
- CalculateNextUpdateTime(UpdateDelay);
+ CalculateNextUpdateTime(isInteractive: true);
}
break;
@@ -268,7 +244,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);
@@ -281,7 +256,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);
@@ -296,16 +270,12 @@ void GitUICommands_PostCheckout(object sender, GitUIPostActionEventArgs e)
{
CurrentStatus = GitStatusMonitorState.Running;
}
-
- void GitUICommands_PostEditGitIgnore(object sender, GitUIEventArgs e)
- {
- _ignoredFiles = new HashSet();
- _ignoredFilesAreStale = true;
- }
}
private void StartWatchingChanges(string workTreePath, string gitDirPath)
{
+ _statusCancellationToken = _statusCancellation.Next();
+
// reset status info, it was outdated
GitWorkingDirectoryStatusChanged?.Invoke(this, new GitWorkingDirectoryStatusEventArgs());
@@ -316,21 +286,10 @@ private void StartWatchingChanges(string workTreePath, string gitDirPath)
{
_workTreeWatcher.Path = workTreePath;
_gitDirWatcher.Path = gitDirPath;
- _globalIgnoreFilePath = DetermineGlobalIgnoreFilePath();
- string globalIgnoreDirectory = Path.GetDirectoryName(_globalIgnoreFilePath);
- if (Directory.Exists(globalIgnoreDirectory))
- {
- _globalIgnoreWatcher.Path = globalIgnoreDirectory;
- }
-
_gitPath = Path.GetDirectoryName(gitDirPath);
_submodulesPath = Path.Combine(_gitPath, "modules");
_currentUpdateInterval = MinUpdateInterval;
_previousUpdateTime = 0;
- _ignoredFilesAreStale = true;
- _ignoredFiles = new HashSet();
- _ignoredFilesTimer.Stop();
- _ignoredFilesTimer.Start();
CurrentStatus = GitStatusMonitorState.Running;
}
else
@@ -344,32 +303,12 @@ private void StartWatchingChanges(string workTreePath, string gitDirPath)
}
return;
-
- string DetermineGlobalIgnoreFilePath()
- {
- // According to https://git-scm.com/docs/git-config, the following are checked in order:
- // - core.excludesFile configuration,
- // - $XDG_CONFIG_HOME/git/ignore, if XDG_CONFIG_HOME is set and not empty,
- // - $HOME/.config/git/ignore.
-
- string globalExcludeFile = Module.GetEffectiveSetting("core.excludesFile");
- if (!string.IsNullOrWhiteSpace(globalExcludeFile))
- {
- return Path.GetFullPath(globalExcludeFile);
- }
-
- string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
- if (!string.IsNullOrWhiteSpace(xdgConfigHome))
- {
- return Path.GetFullPath(Path.Combine(xdgConfigHome, "git/ignore"));
- }
-
- return Path.GetFullPath(Path.Combine(EnvironmentConfiguration.GetHomeDir(), ".config/git/ignore"));
- }
}
private void Update()
{
+ ThreadHelper.AssertOnUIThread();
+
if (CurrentStatus != GitStatusMonitorState.Running)
{
return;
@@ -399,71 +338,93 @@ 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);
}
- 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(PeriodicUpdateInterval);
}
})
.FileAndForget();
- // Schedule update every 5 min, even if we don't know that anything changed
- CalculateNextUpdateTime(MaxUpdatePeriod);
-
return;
- void UpdatedStatusReceived(IReadOnlyList changedFiles)
+ async Task> GetChangedFilesAsync()
+ {
+ try
+ {
+ await TaskScheduler.Default;
+ if (statusCancellationToken.IsCancellationRequested)
+ {
+ return Enumerable.Empty();
+ }
+
+ var cmd = GitCommandHelpers.GetAllChangedFilesCmd(true, UntrackedFilesMode.Default, noLocks: true);
+ var output = module.RunGitCmd(cmd);
+ return GitCommandHelpers.GetStatusChangedFilesFromString(module, output);
+ }
+ catch
+ {
+ try
+ {
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(statusCancellationToken);
+ }
+ catch
+ {
+ }
+ finally
+ {
+ CurrentStatus = GitStatusMonitorState.Stopped;
+ }
+
+ throw;
+ }
+ }
+
+ void UpdatedStatusReceived(IEnumerable 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(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)
{
@@ -472,7 +433,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);
}
}
}
\ No newline at end of file