Skip to content

Handle kFSEventStreamEventFlagMustScanSubDirs to prevent missed UI refreshes#2782

Merged
lucasderraugh merged 1 commit into
git-up:masterfrom
delacrixmorgan:feature/refresh
Jun 10, 2026
Merged

Handle kFSEventStreamEventFlagMustScanSubDirs to prevent missed UI refreshes#2782
lucasderraugh merged 1 commit into
git-up:masterfrom
delacrixmorgan:feature/refresh

Conversation

@delacrixmorgan

Copy link
Copy Markdown
Contributor

Summary

The UI occasionally fails to refresh when files change in the working directory or .git directory. This is caused by the FSEvents callback ignoring kFSEventStreamEventFlagMustScanSubDirs events, which the kernel sends when its event buffer overflows during rapid filesystem activity.

Problem

In GCLiveRepository.m, the _stream:didReceiveEvents:withPaths:flags: method previously discarded events flagged with kFSEventStreamEventFlagMustScanSubDirs:

if (eventFlags[i] & kFSEventStreamEventFlagMustScanSubDirs) {
    XLOG_WARNING(@"Ignoring event stream request to rescan \"%s\"", path);
}

Per Apple's FSEvents documentation, this flag means: "Your application must rescan not just the directory given in the event, but all its children, recursively." It fires when the kernel drops or coalesces events due to buffer overflow — typically during rapid multi-file changes (e.g., git checkout, git rebase, IDE refactoring, build tools).

By ignoring this flag, the app missed those change notifications entirely, leaving the UI stale.

Fix

When kFSEventStreamEventFlagMustScanSubDirs is received, we now mark the appropriate stream as changed and schedule the existing debounced update timer:

if (eventFlags[i] & kFSEventStreamEventFlagMustScanSubDirs) {
    XLOG_WARNING(@"Received event stream request to rescan \"%s\"", path);
    if (stream == _gitDirectoryStream) {
        _gitDirectoryChanged = YES;
    } else {
        _workingDirectoryChanged = YES;
    }
    CFRunLoopTimerSetNextFireDate(_updateTimer, CFAbsoluteTimeGetCurrent() + kUpdateLatency);
}

This follows the exact same code path as regular file change events — no new logic introduced.

Why This Is Safe

  1. Reuses existing infrastructure — sets the same boolean flags and reschedules the same timer used by all normal events.
  2. Debounced — the 0.5s kUpdateLatency timer coalesces multiple triggers; no duplicate processing.
  3. Idempotent downstream_updateStatus: compares the new diff against the current one (isEqualToDiff:) and only notifies the UI if something actually changed. No spurious redraws.
  4. Rare trigger — this flag only fires on kernel buffer overflow, not on every event. Normal single-file edits are unaffected.

Risk Assessment

Risk Likelihood Impact Mitigation
Extra status recomputation on buffer overflow events Low (flag is rare) Negligible (same cost as any file-save event) Existing 0.5s debounce timer coalesces bursts
UI flicker from unnecessary redraws Very Low Low _updateStatus: already diffs against current state and only notifies on actual changes
Regression in ignore-path filtering None N/A For MustScanSubDirs, we intentionally skip the ignore check — the kernel is telling us events were lost, so we must assume something changed. The downstream status computation will correctly reflect only non-ignored files.
Thread safety concerns None N/A All code runs on the main run loop (FSEventStream is scheduled on CFRunLoopGetMain()), same as before

Overall risk: Very Low. The change is minimal, follows established patterns in the codebase, and Apple's documentation explicitly mandates this behavior.

@Cykelero

Cykelero commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

This is a change I've been thinking of making for a while, now, but I've always been bugged by the fact that I don't know why POL, the original author, wrote it this way to begin with; whether it's a mistake, or a deliberate choice.

The only issue I can imagine this fix causing, would be GitUp adding oil to the fire, when the system is already swamped (which it would be, to reach the point of coalescing file system updates like this). Not sure if that's a reasonable concern, or if e.g. the debouncing (kUpdateLatency) would be enough to avoid the issue.

Can you describe when you've hit issues, that you think this patch would resolve? Have you been able to confirm that this fix helps, or is it a best guess at a solution? Having a reliable repro would be a great first step to evaluate the change.

@delacrixmorgan

Copy link
Copy Markdown
Contributor Author

I have to admit that I can't consistently recreate this issue, but it happened twice this week for me where I will need to close the application in order to see the latest code changes, else it'll just at an outdated state.

When I added this fix, I didn't notice any breaking changes.

@lucasderraugh

Copy link
Copy Markdown
Collaborator

Thanks @delacrixmorgan. I've experienced this myself in the Tahoe beta build. I have been meaning to debug why the UI doesn't appear to redraw in these cases and it's the main reason I haven't made the build the stable build yet.

I'll review your changes tonight and see if I can track the refresh cycle with logs to see if I can better understand if this is resolving that issue or if there's something else going on.

@lucasderraugh lucasderraugh merged commit 326b3ae into git-up:master Jun 10, 2026
2 checks passed
@lucasderraugh

Copy link
Copy Markdown
Collaborator

Ultimately, this does seem like the correct solution. Whether it resolves the issue I've seen remains to be seen, but I'm going to put it in and roll a release with it. Thanks again @delacrixmorgan.

@Cykelero

Copy link
Copy Markdown
Contributor

I tried reproducing the issue (in a build of the app without this PR’s fix) but haven’t had any luck. This script makes changes very fast, which does trigger the rescan request; but once the script stops running, GitUp does properly update its workdir diff. I’m wondering what the conditions are exactly.

(place this script in a repo folder, and create a “changing-rapidly” folder next to it)

import Foundation

func write(_ string: String, toFileNumbered fileNumber: Int) {
    let fileURL = scratchDirectoryURL.appendingPathComponent("\(fileNumber).txt")
    try? (string + "\n").data(using: .utf8)?.write(to: fileURL, options: .atomic)
}

let maxFileNumber = 9

let scratchDirectoryURL =
    URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
    .appendingPathComponent("changing-rapidly/")

let startDate = Date()

// Randomly write data for a while
while Date().timeIntervalSince(startDate) < 10 {
    write(
        String(UInt64.random(in: 0...UInt64.max)),
        toFileNumbered: Int.random(in: 0...maxFileNumber)
    )
}

// Write predictable content to all files
for fileNumber in 0...maxFileNumber {
    write(
        "\(Date()) (\(fileNumber)): done",
        toFileNumbered: fileNumber
    )
}

@lucasderraugh

Copy link
Copy Markdown
Collaborator

I hit this case again today, and for me it seems to be not a case of file updates failing, there seems to be some issue when I change the commit I'm pointing to that results in this unrecoverable state. Might end up reverting this change and adding some more logs locally to track this down.

@delacrixmorgan

Copy link
Copy Markdown
Contributor Author

Yeah, I second that as well. I've been facing the same issues lately. :(

@lucasderraugh

Copy link
Copy Markdown
Collaborator

Alright, I took the time to run from Xcode today and was able to figure out the repro case. I'll try to get a build up later tonight with the fixes. I'll probably revert this particular change for now just because we haven't needed it this far and I don't think it's resolving anything.

lucasderraugh added a commit that referenced this pull request Jun 23, 2026
@lucasderraugh

Copy link
Copy Markdown
Collaborator

Reverted this change and also added what I believe was the culprit as a fix: 4787d9a

I'll make a new continuous build tonight.

@lucasderraugh

Copy link
Copy Markdown
Collaborator

Alright, updated https://github.com/git-up/GitUp/releases/tag/b1057
@delacrixmorgan I'm going to probably release this build to stable this week, but let me know if you are still reproducing the issue after this new build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants