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