diff --git a/src/Commands/QueryHasLocalChanges.cs b/src/Commands/QueryHasLocalChanges.cs new file mode 100644 index 000000000..3d73dfdb1 --- /dev/null +++ b/src/Commands/QueryHasLocalChanges.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + /// + /// Lightweight check used by repositories in inactive tabs: only tells whether there are local changes, + /// without parsing them, so the tab's dirty indicator stays up to date. + /// + public class QueryHasLocalChanges : Command + { + public QueryHasLocalChanges(string repo, bool includeUntracked) + { + WorkingDirectory = repo; + Context = repo; + RaiseError = false; + Args = includeUntracked + ? "--no-optional-locks status -unormal --ignore-submodules=dirty --porcelain" + : "--no-optional-locks status -uno --ignore-submodules=dirty --porcelain"; + } + + public async Task GetResultAsync() + { + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (!rs.IsSuccess) + return null; + + return !string.IsNullOrWhiteSpace(rs.StdOut); + } + } +} diff --git a/src/Models/IRepository.cs b/src/Models/IRepository.cs index 2fc7c6125..96a95ff87 100644 --- a/src/Models/IRepository.cs +++ b/src/Models/IRepository.cs @@ -10,6 +10,7 @@ public interface IRepository void RefreshCommits(); void RefreshSubmodules(); void RefreshWorkingCopyChanges(); + void RefreshDirtyState(); void RefreshStashes(); } } diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 66e246bc0..2baaba1f4 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -100,6 +100,15 @@ public IDisposable Lock() return new LockContext(this); } + /// + /// Inactive repositories (not shown in the active tab) still collect changes, but do not refresh + /// anything until they become active again. + /// + public void SetActive(bool active) + { + _isActive = active; + } + public void MarkBranchUpdated() { Interlocked.Exchange(ref _updateBranch, 0); @@ -144,6 +153,12 @@ private void Tick(object sender) if (Interlocked.Read(ref _lockCount) > 0) return; + if (!_isActive) + { + CheckDirtyStateWhileInactive(); + return; + } + var now = DateTime.Now.ToFileTime(); var refreshCommits = false; var refreshSubmodules = false; @@ -228,6 +243,18 @@ private void Tick(object sender) _repo.RefreshCommits(); } + private void CheckDirtyStateWhileInactive() + { + // Pending refreshes are kept as-is so they run once the repository becomes active again. Only update + // the dirty indicator of the tab, once per batch of changes. + var pending = Math.Max(Interlocked.Read(ref _updateWC), Interlocked.Read(ref _updateBranch)); + if (pending <= 0 || pending == _lastDirtyStateCheck || DateTime.Now.ToFileTime() <= pending) + return; + + _lastDirtyStateCheck = pending; + _repo.RefreshDirtyState(); + } + private void OnRepositoryChanged(object o, FileSystemEventArgs e) { if (string.IsNullOrEmpty(e.Name) || e.Name.Equals(".git", StringComparison.Ordinal)) @@ -377,6 +404,8 @@ private bool IsInSubmodule(string folder) private readonly string _root; private List _watchers; private Timer _timer; + private volatile bool _isActive = true; + private long _lastDirtyStateCheck = 0; private long _lockCount; private long _updateWC; diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index aad821cda..7f5ef1d97 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -27,8 +27,17 @@ public LauncherPage ActivePage get => _activePage; set { + var old = _activePage; if (SetProperty(ref _activePage, value)) + { + if (old is { Data: Repository oldRepo }) + oldRepo.SetActive(false); + + if (value is { Data: Repository newRepo }) + newRepo.SetActive(true); + PostActivePageChanged(); + } } } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 4d3b5541c..90f943794 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -504,6 +504,11 @@ public void Close() _autoFetchTimer.Dispose(); } + public void SetActive(bool active) + { + _watcher?.SetActive(active); + } + public void SendNotification(string message, bool isError = false) { Models.Notification.Send(FullPath, message, isError); @@ -1368,6 +1373,23 @@ public void RefreshSubmodules() }); } + public void RefreshDirtyState() + { + if (IsBare) + return; + + var includeUntracked = _uiStates.IncludeUntrackedInLocalChanges; + Task.Run(async () => + { + var hasChanges = await new Commands.QueryHasLocalChanges(FullPath, includeUntracked) + .GetResultAsync() + .ConfigureAwait(false); + + if (hasChanges is { } value) + Dispatcher.UIThread.Post(() => GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasLocalChanges, !value)); + }); + } + public void RefreshWorkingCopyChanges() { if (IsBare)