Watcher.cs
#pragma warning disable 4014
using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Reactive.Linq; using System.Reactive; using System.Timers; using LibGit2Sharp; using static System.Reactive.Linq.Observable; using System.Diagnostics; namespace PurePwsh { public class Watcher : IDisposable { public event EventHandler StatusChanged; public event EventHandler LogEvent; private readonly IDisposable _eventSubscription; private readonly Timer _fetchTimer = new Timer { AutoReset = true }; private readonly FileSystemWatcher _fsw = new FileSystemWatcher() { IncludeSubdirectories = true }; private GitStatus _status = new GitStatus(); public Watcher(string initialDirectory, double fetchInterval = 0) { _eventSubscription = Merge( FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(x => _fsw.Changed += x, x => _fsw.Changed -= x), FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(x => _fsw.Deleted += x, x => _fsw.Deleted -= x) ) .Throttle(TimeSpan.FromSeconds(2.5)) .Subscribe(x => UpdateGitStatus(x.EventArgs)); GitFetchMs = fetchInterval; PwdChanged(initialDirectory); GitFetch(); _fetchTimer.Elapsed += (s, a) => GitFetch(); } public GitStatus Status { get => _status; private set { if (!value.HasChanged(_status)) return; _status = value; if (!string.IsNullOrWhiteSpace(value.GitPath)) StatusChanged?.Invoke(this, EventArgs.Empty); } } public double GitFetchMs { get => _fetchTimer.Interval; set { _fetchTimer.Interval = Math.Max(value, 1); // <= 0 is an ArgumentException _fetchTimer.Enabled = value > 0; } } async Task GitFetch() { if (string.IsNullOrWhiteSpace(_status.GitPath)) return; LogEvent?.Invoke(this, new LogEventArgs("Fetching from git...")); // if there's a problem here, try to avoid taking down the entire session try { await Task.Run( () => { using (var repo = new Repository(_status.GitPath)) { var remote = repo.Network.Remotes["origin"]; if (remote.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { var refSpecs = remote.FetchRefSpecs.Select(x => x.Specification); Commands.Fetch(repo, remote.Name, refSpecs, null, ""); } else { LogEvent?.Invoke(this, new LogEventArgs( "SSH fetch not supported in libgit2. Invoking git directly.")); Process.Start( new ProcessStartInfo { WorkingDirectory = _status.GitPath, FileName = "git", Arguments = "fetch", UseShellExecute = false, CreateNoWindow = true }); } } }); } catch (Exception ex) { LogEvent?.Invoke(this, new LogEventArgs($"Error fetching from git: {ex}")); } } async Task UpdateGitStatus(FileSystemEventArgs args = null, string path = null) { path = path ?? _status.GitPath; if (string.IsNullOrWhiteSpace(path)) return; LogEvent?.Invoke(this, new LogEventArgs($"{args?.FullPath ?? "(pwd)"} was modified.")); // if there's a problem here, try to avoid taking down the entire session try { await Task.Run( () => { using (var repo = new Repository(path)) { var status = repo.RetrieveStatus(new StatusOptions { IncludeIgnored = false }); var branch = repo.Head; var ahead = branch.TrackingDetails.AheadBy; var behind = branch.TrackingDetails.BehindBy; Status = new GitStatus { Dirty = status.IsDirty, Ahead = ahead > 0, Behind = behind > 0, BranchName = branch.FriendlyName, GitPath = repo.Info.Path }; } } ); } catch (Exception ex) { LogEvent?.Invoke(this, new LogEventArgs($"Error updating git status: {ex}")); } } public async Task PwdChanged(string newDirectory) { var maybeRepoPath = Repository.Discover(newDirectory); if (maybeRepoPath != null && maybeRepoPath != Status.GitPath) { _fsw.Path = Directory.GetParent(Path.GetDirectoryName(maybeRepoPath)).FullName; _fsw.EnableRaisingEvents = true; await UpdateGitStatus(path: maybeRepoPath); } else { _fsw.EnableRaisingEvents = false; Status = new GitStatus(); } } public void Dispose() { _fsw?.Dispose(); _eventSubscription?.Dispose(); } public override string ToString() { return Status.ToString(); } } public class GitStatus { public bool Dirty; public bool Ahead; public bool Behind; public string BranchName; public string GitPath; public override string ToString() { return $"{BranchName} ({(Dirty ? "!" : "")}{(Ahead ? "+" : "")}{(Behind ? "-" : "")})"; } public bool HasChanged(GitStatus other) => other.Dirty != Dirty || other.Ahead != Ahead || other.Behind != Behind || other.BranchName != BranchName || other.GitPath != GitPath; } public class LogEventArgs : EventArgs { public LogEventArgs(string output) => Output = output; public string Output { get; } } } |