From 97ae26398676271d8e3215f7314f1ac5a04cd715 Mon Sep 17 00:00:00 2001 From: Gadfly Date: Tue, 21 Apr 2026 01:46:45 +0800 Subject: [PATCH] fix: prevent NullReferenceException in Histories async operations Add null checks for _repo in Histories methods to prevent crashes when Repository is closed while async operations are still pending. Bug Cause: When Repository.Close() is called, Histories.Dispose() sets _repo to null. If an async operation (e.g., CheckoutBranchByDecoratorAsync triggered by double-clicking a branch icon) is executing concurrently, it may access the now-null _repo reference, causing NullReferenceException. Stack Trace: ```log System.NullReferenceException: Object reference not set to an instance of an object. at SourceGit.ViewModels.Repository.d__192.MoveNext() + 0x169 --- End of stack trace from previous location --- at SourceGit.ViewModels.Histories.d__48.MoveNext() + 0x57 --- End of stack trace from previous location --- at SourceGit.Views.Histories.d__34.MoveNext() + 0x5a --- End of stack trace from previous location --- at System.Threading.Tasks.Task.<>c.b__124_0(Object state) + 0x1a at Avalonia.Threading.SendOrPostCallbackDispatcherOperation.InvokeCore() + 0x19a at Avalonia.Threading.DispatcherOperation.Execute() + 0x4a at Avalonia.Threading.Dispatcher.ExecuteJob(DispatcherOperation) + 0x78 at Avalonia.Threading.Dispatcher.ExecuteJobsCore(Boolean) + 0x387 at Avalonia.Win32.Win32Platform.WndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam) + 0x58 at SourceGit!+0x1166c9a ``` --- src/ViewModels/Histories.cs | 98 +++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 4d0fd02f9..de2a928a6 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -143,6 +143,12 @@ public void Dispose() public Models.BisectState UpdateBisectInfo() { + if (_repo == null) + { + Bisect = null; + return Models.BisectState.None; + } + var test = Path.Combine(_repo.GitDir, "BISECT_START"); if (!File.Exists(test)) { @@ -182,6 +188,9 @@ public void NavigateTo(string commitSHA) return; } + if (_repo == null) + return; + Task.Run(async () => { var c = await new Commands.QuerySingleCommit(_repo.FullPath, commitSHA) @@ -190,6 +199,9 @@ public void NavigateTo(string commitSHA) Dispatcher.UIThread.Post(() => { + if (_repo == null) + return; + _ignoreSelectionChange = true; SelectedCommit = null; @@ -263,7 +275,7 @@ public void Select(IList commits) public async Task CheckoutBranchByDecoratorAsync(Models.Decorator decorator) { - if (decorator == null) + if (decorator == null || _repo == null) return false; if (decorator.Type == Models.DecoratorType.CurrentBranchHead || @@ -310,7 +322,7 @@ public async Task CheckoutBranchByDecoratorAsync(Models.Decorator decorato public async Task CheckoutBranchByCommitAsync(Models.Commit commit) { - if (commit.IsCurrentHead) + if (commit.IsCurrentHead || _repo == null) return; Models.Branch firstRemoteBranch = null; @@ -355,61 +367,64 @@ public async Task CheckoutBranchByCommitAsync(Models.Commit commit) public async Task CherryPickAsync(Models.Commit commit) { - if (_repo.CanCreatePopup()) + if (_repo == null || !_repo.CanCreatePopup()) + return; + + if (commit.Parents.Count <= 1) { - if (commit.Parents.Count <= 1) - { - _repo.ShowPopup(new CherryPick(_repo, [commit])); - } - else + _repo.ShowPopup(new CherryPick(_repo, [commit])); + } + else + { + var parents = new List(); + foreach (var sha in commit.Parents) { - var parents = new List(); - foreach (var sha in commit.Parents) - { - var parent = _commits.Find(x => x.SHA.Equals(sha, StringComparison.Ordinal)); - if (parent == null) - parent = await new Commands.QuerySingleCommit(_repo.FullPath, sha).GetResultAsync(); - - if (parent != null) - parents.Add(parent); - } + var parent = _commits.Find(x => x.SHA.Equals(sha, StringComparison.Ordinal)); + if (parent == null) + parent = await new Commands.QuerySingleCommit(_repo.FullPath, sha).GetResultAsync(); - _repo.ShowPopup(new CherryPick(_repo, commit, parents)); + if (parent != null) + parents.Add(parent); } + + _repo.ShowPopup(new CherryPick(_repo, commit, parents)); } } public async Task RewordHeadAsync(Models.Commit head) { - if (_repo.CanCreatePopup()) - { - var message = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).GetResultAsync(); - _repo.ShowPopup(new Reword(_repo, head, message)); - } + if (_repo == null || !_repo.CanCreatePopup()) + return; + + var message = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).GetResultAsync(); + _repo.ShowPopup(new Reword(_repo, head, message)); } public async Task SquashOrFixupHeadAsync(Models.Commit head, bool fixup) { - if (head.Parents.Count == 1) - { - var parent = await new Commands.QuerySingleCommit(_repo.FullPath, head.Parents[0]).GetResultAsync(); - if (parent == null) - return; + if (_repo == null || head.Parents.Count != 1) + return; - string message = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.Parents[0]).GetResultAsync(); - if (!fixup) - { - var headMessage = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).GetResultAsync(); - message = $"{message}\n\n{headMessage}"; - } + var parent = await new Commands.QuerySingleCommit(_repo.FullPath, head.Parents[0]).GetResultAsync(); + if (parent == null) + return; - if (_repo.CanCreatePopup()) - _repo.ShowPopup(new SquashOrFixupHead(_repo, parent, message, fixup)); + string message = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.Parents[0]).GetResultAsync(); + if (!fixup) + { + var headMessage = await new Commands.QueryCommitFullMessage(_repo.FullPath, head.SHA).GetResultAsync(); + message = $"{message}\n\n{headMessage}"; } + + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new SquashOrFixupHead(_repo, parent, message, fixup)); } public async Task DropHeadAsync(Models.Commit head) { + if (_repo == null) + return; + var parent = _commits.Find(x => x.SHA.Equals(head.Parents[0])); if (parent == null) parent = await new Commands.QuerySingleCommit(_repo.FullPath, head.Parents[0]).GetResultAsync(); @@ -420,6 +435,9 @@ public async Task DropHeadAsync(Models.Commit head) public async Task GetCommitFullMessageAsync(Models.Commit commit) { + if (_repo == null) + return string.Empty; + return await new Commands.QueryCommitFullMessage(_repo.FullPath, commit.SHA) .GetResultAsync() .ConfigureAwait(false); @@ -427,6 +445,9 @@ public async Task GetCommitFullMessageAsync(Models.Commit commit) public async Task CompareWithHeadAsync(Models.Commit commit) { + if (_repo == null) + return null; + var head = _commits.Find(x => x.IsCurrentHead); if (head == null) { @@ -443,7 +464,8 @@ public async Task GetCommitFullMessageAsync(Models.Commit commit) public void CompareWithWorktree(Models.Commit commit) { - DetailContext = new RevisionCompare(_repo, commit, null); + if (_repo != null) + DetailContext = new RevisionCompare(_repo, commit, null); } private Repository _repo = null;