Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 96 additions & 25 deletions src/Models/Watcher.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -18,7 +19,9 @@ public LockContext(Watcher target)

public void Dispose()
{
Interlocked.Decrement(ref _target._lockCount);
var target = Interlocked.Exchange(ref _target, null);
if (target != null && Interlocked.Decrement(ref target._lockCount) == 0)
target.RefreshAfterOperation();
}

private Watcher _target;
Expand Down Expand Up @@ -79,27 +82,63 @@ public Watcher(IRepository repo, string fullpath, string gitDir)
}

_timer = new Timer(Tick, null, 100, 100);
ViewModels.Preferences.Instance.PropertyChanged += OnPreferencesChanged;

// Starts filesystem watchers in another thread to avoid UI blocking
Task.Run(() =>
Task.Run(UpdateWatching);
}

private void UpdateWatching()
{
lock (_syncRoot)
{
if (_disposed)
return;

try
{
foreach (var watcher in _watchers)
watcher.EnableRaisingEvents = true;
watcher.EnableRaisingEvents = !ViewModels.Preferences.Instance.ManualRefresh;
}
catch
{
// Ignore exceptions. This may occur while `Dispose` is called.
// Ignore failures while starting filesystem watchers.
}
});
}
}

private void OnPreferencesChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(ViewModels.Preferences.ManualRefresh))
return;

UpdateWatching();
if (!ViewModels.Preferences.Instance.ManualRefresh)
OnWatcherError(null, null); // Reload changes missed while filesystem watchers were disabled.
}

public IDisposable Lock()
{
return new LockContext(this);
}

private void RefreshAfterOperation()
{
// Application operations use Lock() while changing the repository. In manual mode
// there are no filesystem events to trigger their final refresh, so do it explicitly
// when the outermost operation completes, including failed or cancelled operations.
if (_disposed || !ViewModels.Preferences.Instance.ManualRefresh)
return;

_repo.RefreshBranches();
_repo.RefreshWorktrees();
_repo.RefreshWorkingCopyChanges();
_repo.RefreshSubmodules();
_repo.RefreshStashes();
_repo.RefreshTags();
_repo.RefreshCommits();
}

public void MarkBranchUpdated()
{
Interlocked.Exchange(ref _updateBranch, 0);
Expand Down Expand Up @@ -128,23 +167,36 @@ public void MarkSubmodulesUpdated()

public void Dispose()
{
foreach (var watcher in _watchers)
ViewModels.Preferences.Instance.PropertyChanged -= OnPreferencesChanged;
lock (_syncRoot)
{
watcher.EnableRaisingEvents = false;
watcher.Dispose();
}
if (_disposed)
return;

_watchers.Clear();
_timer.Dispose();
_timer = null;
_disposed = true;
foreach (var watcher in _watchers)
{
watcher.EnableRaisingEvents = false;
watcher.Dispose();
}

_watchers.Clear();
_timer.Dispose();
_timer = null;
}
}

private void Tick(object sender)
{
if (Interlocked.Read(ref _lockCount) > 0)
if (ViewModels.Preferences.Instance.ManualRefresh || Interlocked.Read(ref _lockCount) > 0)
return;

var now = DateTime.Now.ToFileTime();
// Wait for a quiet period across all event types before refreshing anything.
var quietPeriod = (long)ViewModels.Preferences.Instance.RefreshThrottling * TimeSpan.TicksPerMillisecond;
if (now - Interlocked.Read(ref _lastChange) <= quietPeriod)
return;

var refreshCommits = false;
var refreshSubmodules = false;
var refreshWC = false;
Expand Down Expand Up @@ -249,7 +301,10 @@ private void OnRepositoryChanged(object o, FileSystemEventArgs e)
/// </summary>
private void OnWatcherError(object o, ErrorEventArgs e)
{
var desired = DateTime.Now.AddSeconds(1).ToFileTime();
if (ViewModels.Preferences.Instance.ManualRefresh)
return;

var desired = RecordChange();
Interlocked.Exchange(ref _updateBranch, desired);
Interlocked.Exchange(ref _updateTags, desired);
Interlocked.Exchange(ref _updateStashes, desired);
Expand Down Expand Up @@ -283,6 +338,9 @@ private void OnWorkingCopyChanged(object o, FileSystemEventArgs e)

private void HandleGitDirFileChanged(string name)
{
if (ViewModels.Preferences.Instance.ManualRefresh)
return;

if (name.Contains("fsmonitor--daemon/", StringComparison.Ordinal) ||
name.EndsWith(".lock", StringComparison.Ordinal) ||
name.StartsWith("lfs/", StringComparison.Ordinal))
Expand All @@ -293,7 +351,7 @@ private void HandleGitDirFileChanged(string name)
if (name.EndsWith("/HEAD", StringComparison.Ordinal) ||
name.EndsWith("/ORIG_HEAD", StringComparison.Ordinal))
{
var desired = DateTime.Now.AddSeconds(1).ToFileTime();
var desired = RecordChange();
Interlocked.Exchange(ref _updateSubmodules, desired);
Interlocked.Exchange(ref _updateWC, desired);
}
Expand All @@ -302,51 +360,54 @@ private void HandleGitDirFileChanged(string name)
name.Equals("AUTO_MERGE", StringComparison.Ordinal))
{
if (_repo.MayHaveSubmodules())
Interlocked.Exchange(ref _updateSubmodules, DateTime.Now.AddSeconds(1).ToFileTime());
Interlocked.Exchange(ref _updateSubmodules, RecordChange());
}
else if (name.StartsWith("refs/tags", StringComparison.Ordinal))
{
Interlocked.Exchange(ref _updateTags, DateTime.Now.AddSeconds(.5).ToFileTime());
Interlocked.Exchange(ref _updateTags, RecordChange());
}
else if (name.StartsWith("refs/stash", StringComparison.Ordinal) || name.Equals("logs/refs/stash", StringComparison.Ordinal))
{
Interlocked.Exchange(ref _updateStashes, DateTime.Now.AddSeconds(.5).ToFileTime());
Interlocked.Exchange(ref _updateStashes, RecordChange());
}
else if (name.Equals("HEAD", StringComparison.Ordinal) ||
name.Equals("BISECT_START", StringComparison.Ordinal) ||
name.StartsWith("refs/heads/", StringComparison.Ordinal) ||
name.StartsWith("refs/remotes/", StringComparison.Ordinal) ||
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))
{
Interlocked.Exchange(ref _updateBranch, DateTime.Now.AddSeconds(.5).ToFileTime());
Interlocked.Exchange(ref _updateBranch, RecordChange());
}
else if (name.Equals("packed-refs", StringComparison.Ordinal))
{
var desired = DateTime.Now.AddSeconds(.5).ToFileTime();
var desired = RecordChange();
Interlocked.Exchange(ref _updateBranch, desired);
Interlocked.Exchange(ref _updateTags, desired);
}
else if (name.StartsWith("reftable/", StringComparison.Ordinal))
{
var desired = DateTime.Now.AddSeconds(.5).ToFileTime();
var desired = RecordChange();
Interlocked.Exchange(ref _updateBranch, desired);
Interlocked.Exchange(ref _updateTags, desired);
Interlocked.Exchange(ref _updateStashes, desired);
}
else if (name.StartsWith("objects/", StringComparison.Ordinal) || name.Equals("index", StringComparison.Ordinal))
{
Interlocked.Exchange(ref _updateWC, DateTime.Now.AddSeconds(1).ToFileTime());
Interlocked.Exchange(ref _updateWC, RecordChange());
}
}

private void HandleWorkingCopyFileChanged(string name, string fullpath)
{
if (ViewModels.Preferences.Instance.ManualRefresh)
return;

if (name.StartsWith(".vs/", StringComparison.Ordinal))
return;

if (name.Equals(".gitmodules", StringComparison.Ordinal))
{
var desired = DateTime.Now.AddSeconds(1).ToFileTime();
var desired = RecordChange();
Interlocked.Exchange(ref _updateSubmodules, desired);
Interlocked.Exchange(ref _updateWC, desired);
return;
Expand All @@ -355,11 +416,11 @@ private void HandleWorkingCopyFileChanged(string name, string fullpath)
var dir = Directory.Exists(fullpath) ? fullpath : Path.GetDirectoryName(fullpath);
if (IsInSubmodule(dir))
{
Interlocked.Exchange(ref _updateSubmodules, DateTime.Now.AddSeconds(1).ToFileTime());
Interlocked.Exchange(ref _updateSubmodules, RecordChange());
return;
}

Interlocked.Exchange(ref _updateWC, DateTime.Now.AddSeconds(1).ToFileTime());
Interlocked.Exchange(ref _updateWC, RecordChange());
}

private bool IsInSubmodule(string folder)
Expand All @@ -373,12 +434,22 @@ private bool IsInSubmodule(string folder)
return IsInSubmodule(Path.GetDirectoryName(folder));
}

private long RecordChange()
{
var now = DateTime.Now.ToFileTime();
Interlocked.Exchange(ref _lastChange, now);
return now;
}

private readonly IRepository _repo;
private readonly object _syncRoot = new object();
private bool _disposed;
private readonly string _root;
private List<FileSystemWatcher> _watchers;
private Timer _timer;

private long _lockCount;
private long _lastChange;
private long _updateWC;
private long _updateBranch;
private long _updateSubmodules;
Expand Down
2 changes: 2 additions & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,9 @@
<x:String x:Key="Text.Preferences.General.DateFormat" xml:space="preserve">Date Format</x:String>
<x:String x:Key="Text.Preferences.General.EnableCompactFolders" xml:space="preserve">Enable compact folders in changes tree</x:String>
<x:String x:Key="Text.Preferences.General.Locale" xml:space="preserve">Language</x:String>
<x:String x:Key="Text.Preferences.General.ManualRefresh" xml:space="preserve">Manual refresh</x:String>
<x:String x:Key="Text.Preferences.General.MaxHistoryCommits" xml:space="preserve">History Commits</x:String>
<x:String x:Key="Text.Preferences.General.RefreshThrottling" xml:space="preserve">Refresh throttling</x:String>
<x:String x:Key="Text.Preferences.General.ShowChangesPageByDefault" xml:space="preserve">Show `LOCAL CHANGES` page by default</x:String>
<x:String x:Key="Text.Preferences.General.ShowChangesTabInCommitDetailByDefault" xml:space="preserve">Show `CHANGES` tab in commit detail by default</x:String>
<x:String x:Key="Text.Preferences.General.ShowRelativeTimeInGraph" xml:space="preserve">Show relative time in commit graph</x:String>
Expand Down
1 change: 1 addition & 0 deletions src/ViewModels/AssumeUnchangedManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public async Task RemoveAsync(string file)
{
if (!string.IsNullOrEmpty(file))
{
using var lockWatcher = _repo.LockWatcher();
var log = _repo.CreateLog("Remove Assume Unchanged File");

await new Commands.AssumeUnchanged(_repo.FullPath, file, false)
Expand Down
16 changes: 15 additions & 1 deletion src/ViewModels/Preferences.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
Expand Down Expand Up @@ -135,6 +135,18 @@ public int MaxHistoryCommits
set => SetProperty(ref _maxHistoryCommits, value);
}

public int RefreshThrottling
{
get => _refreshThrottling;
set => SetProperty(ref _refreshThrottling, Math.Clamp(value, 0, 5000));
}

public bool ManualRefresh
{
get => _manualRefresh;
set => SetProperty(ref _manualRefresh, value);
}

public int SubjectGuideLength
{
get => _subjectGuideLength;
Expand Down Expand Up @@ -801,6 +813,8 @@ private bool RemoveInvalidRepositoriesRecursive(List<RepositoryNode> collection)
private LayoutInfo _layout = new();

private int _maxHistoryCommits = 20000;
private int _refreshThrottling = 1000;
private bool _manualRefresh = false;
private int _subjectGuideLength = 50;
private bool _useFixedTabWidth = true;
private bool _useAutoHideScrollBars = true;
Expand Down
2 changes: 2 additions & 0 deletions src/ViewModels/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ public async Task InstallLFSAsync()

public async Task<bool> TrackLFSFileAsync(string pattern, bool isFilenameMode)
{
using var lockWatcher = LockWatcher();
var log = CreateLog("Track LFS");
var succ = await new Commands.LFS(FullPath)
.Use(log)
Expand Down Expand Up @@ -1924,6 +1925,7 @@ private async Task AutoFetchOnUIThread()
if (remotes.Count == 0)
return;

using var lockWatcher = LockWatcher();
IsAutoFetching = true;
log = CreateLog("Auto-Fetch");

Expand Down
Loading