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)