GitFork is a macOS-only Git GUI inspired by Fork. It is a native Swift and
SwiftUI application backed by /usr/bin/git.
- Minimum deployment target: macOS 14
- Package manager: Swift Package Manager
- UI: SwiftUI, with AppKit where macOS-specific integration is required
- Tests: Swift Testing
- External dependencies: none
Do not introduce JavaScript or TypeScript. Prefer Swift for all application and tooling code. Add Rust only when there is a demonstrated systems-level need that Swift cannot reasonably satisfy.
Sources/GitFork/GitForkApp.swift: app entry point, scenes, menus, and settingsSources/GitFork/RepositoryStore.swift:@MainActorper-window state and UI actionsSources/GitFork/RepositoryWindows.swift: repository window value, routing, and the registry that keeps one window per repositorySources/GitFork/GitClient.swift: asynchronous/usr/bin/gitprocess wrapperSources/GitFork/GitCommandLog.swift,GitCommandLogView.swift: the session's Git command log, its read/write/delete/network labels, and the Git Command Log windowSources/GitFork/CLIInstaller.swift: in-appforkhelper installation UI and serviceSources/ForkCLIKit/: command parsing, repository discovery, and app launch URLSources/ForkCLI/:forkexecutable entry pointSources/GitFork/TerminalColors.swift,Look.swift: colors read from the user's Ghostty theme, and the observableLookthat applies themSources/GitFork/Models.swift: Git domain models and output parsersSources/GitFork/*View.swift: SwiftUI screens and reusable UITests/GitForkTests/: parser and real-repository integration testsTests/ForkCLIKitTests/: CLI parsing and repository discovery testsResources/Info.plist: macOS app-bundle metadatascripts/build-app.sh: release build, app packaging, and ad-hoc signing
Keep Git process execution out of views. Views call RepositoryStore;
RepositoryStore coordinates GitClient and publishes state.
Run commands from the repository root:
make build
make test
make app
make installUseful direct commands:
swift test --disable-sandbox
./scripts/build-app.sh
codesign --verify --deep --strict --verbose=2 .build/release/GitFork.appThe packaged application is written to:
.build/release/GitFork.app
make install replaces $HOME/Applications/GitFork.app with the newly built,
signature-verified bundle.
.build/ is generated and must not be committed.
In restricted environments, use writable Swift and Clang cache directories if the default user cache is unavailable:
CLANG_MODULE_CACHE_PATH=/tmp/gitfork-clang-cache \
SWIFTPM_CUSTOM_CACHE_PATH=/tmp/gitfork-swiftpm-cache \
swift test --disable-sandbox- Keep UI-facing state on
@MainActor. - Run blocking
Processand pipe reads away from the main actor. - Prefer small value types conforming to
Sendable,Hashable, andIdentifiablewhere appropriate. - Use structured concurrency for independent Git reads.
- Surface Git failures through
GitOperationError; do not silently discard failures from mutating commands. - Preserve
LC_ALL=Cfor stable, parseable Git output. - Preserve
GIT_TERMINAL_PROMPT=0so the GUI never hangs on a terminal prompt. - Pass file paths after
--in Git commands. - Prefer NUL-delimited Git output when parsing paths so spaces and unusual filenames remain safe.
- Use native SwiftUI and AppKit controls and standard macOS interaction patterns.
- Colors follow the user's Ghostty theme, as in Agentz:
Lookreads the Ghostty config (falling back to Alabaster or Afterglow) and the theme's brightness sets each window's light or dark appearance. Paint surfaces withLook.shared(background, sidebar, text) and status colors withGitForkTheme, not system background colors or materials. Lists with a selection draw it themselves throughLookListRowBackgroundorSystemSelectionHighlightRemover; the system highlight's blue and white text ignore the theme. - Use SF Symbols instead of custom raster icons when a suitable symbol exists.
- Add
.help(...)tooltips to icon-only and compact action controls. In the window toolbar, use.instantHelp(...)instead: unlabeled icons must explain themselves without AppKit's roughly one-second tooltip delay. - Keep window-toolbar actions in the single leading group, ordered by what they
touch and separated by
ToolbarSeparator. The centered badge reports HEAD and carries no actions. - Give clickable rows and compact controls a visible hover state with pressed and disabled feedback; tooltips alone are not sufficient affordance.
- Support light mode, dark mode, keyboard navigation, and text selection in commit and diff views.
- Keep the sidebar and commit-list columns bounded. Leave the detail column
unsized in
NavigationSplitViewso it consumes remaining window width. - A horizontally scrolling diff must still have a minimum content width equal to its viewport; short diffs should fill the detail pane instead of appearing as a narrow strip.
- A short diff must also fill the viewport height and remain aligned to
.topLeading; do not allow bidirectional scrolling to center it vertically. - The changes view wraps diff text in both plain and stage/unstage modes and scrolls vertically only; history diffs keep horizontal scrolling. Wrapped rows are measured with the same TextKit layout the text is drawn with, so the line-number gutter, row backgrounds, and line selection stay aligned when a line spans several visual rows.
- Repository switching uses the persisted
recentRepositorieslist. Clearly mark the active repository and retain an “Open Other Repository…” action. Linked worktrees are never recorded in or shown from that list; they are reached through their repository's worktree menu. - Each window owns a
RepositoryStoreand shows one repository, and one repository is never open in two windows. Switching repositories opens or focuses another window; only a window with no repository adopts one in place. Switching worktrees, from the toolbar worktree menu or the sidebar, replaces the window's repository in place (placement: .replaceCurrentWindow), and both offer "Open in New Window" for the other worktrees. A worktree another window already shows is brought forward instead. Route every user-initiated open throughrequestOpenRepository(_:placement:);openRepository(_:)loads into the window that already owns the request. - The
forkhelper is bundled underContents/Helpersand installed from the application menu. Keep itsgitfork://open?path=...contract synchronized withGitForkExternalURL. - Use confirmation UI before adding destructive working-tree operations.
- Dragging a local branch onto a sidebar branch folder at any depth moves it under that prefix, and dropping it on the Branches header moves it to the top level. The branch row's "Move to Prefix" menu offers the same moves: the top level, every existing prefix sorted by name, and a new prefix. The branch keeps its last path component, and the rename always waits for confirmation.
- Sidebar rows that can be dragged handle clicks with a tap gesture instead of
a
Button, because aButtonkeeps the mouse and a drag never starts.
- Reads may run concurrently when they do not mutate repository state.
- Refresh repository state after every mutating operation.
- Pulls are fast-forward-only unless product behavior is deliberately changed.
- A first push may establish an upstream using the repository's first remote.
- The commit composer owns a persistent OpenPGP signing choice. Enabled commits
use
-c gpg.format=openpgp --gpg-sign; disabled commits use--no-gpg-signso the UI remains authoritative over globalcommit.gpgSign. - Resolve
gpg.openpgp.programorgpg.programto an absolute executable before signing. GUI-launched Git processes must augmentPATHwith Homebrew and common user binary directories. - Load signature state with Git's
%G?,%GK, and%GSpretty-format fields. Show signed commits in history and the detail header; a cachedgpg-agentpassphrase means signing may legitimately complete without a prompt. - Do not add commands that can wait for interactive terminal input.
- Every command
GitClientruns is recorded inGitCommandLog. Background change polling runs underGitCommandLogScope.isSuppressedand is not logged. The log is saved as JSON Lines in~/Library/Application Support/GitFork/git-command-log.jsonland keeps today and the six days before it. When adding a Git command, check thatGitCommandKind.classifylabels it correctly, and add a case toGitCommandLogTests; anything that removes a branch, tag, stash, worktree, or uncommitted change isdelete. - Never discard changes, delete branches, rewrite history, or force-push without an explicit user action and appropriate confirmation.
- Keep branch, remote, tag, status, log, and diff parsing covered by focused tests when their formats change.
For every code change:
- Run
git diff --check. - Run
make test. - Build the release app with
make appfor UI, packaging, or app-lifecycle changes. - Verify the app signature after packaging.
Add or update tests for Git parsing and command behavior. Integration tests
should create isolated repositories beneath FileManager.default.temporaryDirectory
and remove them after completion.
Do not commit generated build output, user-specific Xcode state, temporary repositories, or signing credentials.