Alert sheet remains stuck when deleting a commit that causes conflicts
Summary
When deleting a commit that is also the tip of a local branch, GitUp shows a confirmation sheet asking whether to delete the branch or the commit. Choosing Delete Commit can trigger merge conflicts during history rewrite. The conflict resolver view appears underneath, but the confirmation sheet stays on screen and its buttons stop responding, so the sheet cannot be dismissed normally.
Environment
- GitUp version: v1.5.0
- Platform: macOS (AppKit sheet + nested modal run loop)
Expected behavior
- The confirmation sheet dismisses after the user chooses Delete Commit.
- If rewriting history produces conflicts, GitUp switches to the conflict resolver view.
- The user can resolve or cancel conflicts without a leftover modal sheet.
Actual behavior
- After choosing Delete Commit, the conflict resolver view appears.
- The confirmation sheet remains visible and its buttons no longer respond.
- The conflict resolver behind the sheet still accepts clicks.
- Pressing Cancel in the conflict resolver eventually allows the stuck sheet to go away.
Steps to reproduce
Create a temporary empty directory and run:
git init -b main
echo test1 > test.txt
git add test.txt
git commit -m "initial commit"
git branch test-branch
git checkout test-branch
echo test2 >> test.txt
git add test.txt
git commit -m "append test2"
git checkout main
echo test3 >> test.txt
git add test.txt
git commit -m "append test3"
git merge test-branch
Resolve the conflict and finish the merge:
echo test1 > test.txt
echo test2 >> test.txt
echo test3 >> test.txt
git add test.txt
git commit -m "merge test-branch into main"
Then:
- Open this repository in GitUp.
- In the Map view, select the append test2 commit on test-branch.
- Right-click and choose Delete.
- In the confirmation sheet (Delete Local Branch / Delete Commit / Cancel), click Delete Commit.
- Observe that the conflict resolver appears while the confirmation sheet stays stuck and unresponsive.
Workaround
Click Cancel in the conflict resolver view behind the sheet. That can dismiss the stuck confirmation sheet.
Suspected root cause
deleteSelectedCommit: presents the confirmation as an NSAlert sheet via beginSheetModalForWindow:completionHandler:. Inside that completion handler it calls deleteCommit: synchronously.
When rewriting history hits conflicts, Document’s resolveMergeConflictsWithOurCommit:theirCommit: enters a nested event loop on NSModalPanelRunLoopMode before the alert sheet has fully finished dismissing. That nested loop conflicts with AppKit sheet teardown, leaving the confirmation sheet stuck on screen.
Relevant call site (pre-fix pattern):
// GIMapViewController.m - deleteSelectedCommit:
[self presentAlert:alert
completionHandler:^(NSInteger returnCode) {
if (returnCode == NSAlertFirstButtonReturn) {
[self deleteLocalBranch:localBranch];
} else if (returnCode == NSAlertSecondButtonReturn) {
[self deleteCommit:commit];
}
}];
Conflict resolution nested loop:
// Document.m - resolveMergeConflictsWithOurCommit:theirCommit:
_resolvingConflicts = 0;
while (!_resolvingConflicts) {
NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantFuture]
inMode:NSModalPanelRunLoopMode
dequeue:YES];
[NSApp sendEvent:event];
}
(There is already a TODO in that method questioning whether re-entering NSApp’s event loop is AppKit-safe.)
Suggested fix
Defer deleteLocalBranch: / deleteCommit: to the next main-queue turn so the alert sheet can finish dismissing before conflict resolution may enter the nested modal run loop:
[self presentAlert:alert
completionHandler:^(NSInteger returnCode) {
// Defer so the alert sheet finishes dismissing before conflict resolution
// may enter a nested NSModalPanelRunLoopMode event loop.
if (returnCode == NSAlertFirstButtonReturn) {
dispatch_async(dispatch_get_main_queue(), ^{
[self deleteLocalBranch:localBranch];
});
} else if (returnCode == NSAlertSecondButtonReturn) {
dispatch_async(dispatch_get_main_queue(), ^{
[self deleteCommit:commit];
});
}
}];
Notes
- Reproduced when the selected commit is a local branch tip (so the branch-vs-commit confirmation sheet is shown). Deleting a commit without that sheet may not hit this exact UI hang.
- A similar pattern exists for the fast-forward confirmation sheet in
smartMergeCommitOrBranch: (choosing Merge can also enter conflict resolution from an alert completion handler).
Alert sheet remains stuck when deleting a commit that causes conflicts
Summary
When deleting a commit that is also the tip of a local branch, GitUp shows a confirmation sheet asking whether to delete the branch or the commit. Choosing Delete Commit can trigger merge conflicts during history rewrite. The conflict resolver view appears underneath, but the confirmation sheet stays on screen and its buttons stop responding, so the sheet cannot be dismissed normally.
Environment
Expected behavior
Actual behavior
Steps to reproduce
Create a temporary empty directory and run:
Resolve the conflict and finish the merge:
Then:
Workaround
Click Cancel in the conflict resolver view behind the sheet. That can dismiss the stuck confirmation sheet.
Suspected root cause
deleteSelectedCommit:presents the confirmation as anNSAlertsheet viabeginSheetModalForWindow:completionHandler:. Inside that completion handler it callsdeleteCommit:synchronously.When rewriting history hits conflicts,
Document’sresolveMergeConflictsWithOurCommit:theirCommit:enters a nested event loop onNSModalPanelRunLoopModebefore the alert sheet has fully finished dismissing. That nested loop conflicts with AppKit sheet teardown, leaving the confirmation sheet stuck on screen.Relevant call site (pre-fix pattern):
Conflict resolution nested loop:
(There is already a TODO in that method questioning whether re-entering NSApp’s event loop is AppKit-safe.)
Suggested fix
Defer
deleteLocalBranch:/deleteCommit:to the next main-queue turn so the alert sheet can finish dismissing before conflict resolution may enter the nested modal run loop:Notes
smartMergeCommitOrBranch:(choosing Merge can also enter conflict resolution from an alert completion handler).