- Overview
- Architecture Principles
- Layer Structure
- Core Components
- Design Patterns
- Data Flow
- Testing Strategy
- Key Abstractions
Xit is a visual Git client for macOS written in Swift, leveraging libgit2 for Git operations and a hybrid AppKit/SwiftUI UI layer. The architecture emphasizes:
- Protocol-oriented design for testability and modularity
- Reactive data flow using Combine publishers
- Separation of concerns between Git operations, business logic, and UI
- Thread-safe repository operations with explicit write control
Xit extensively uses protocols to abstract functionality:
- Repository capabilities are split into focused protocols (
Branching,CommitStorage,FileStaging, etc.) - Fake implementations enable comprehensive unit testing without touching actual Git repositories
- Composition over inheritance via protocol composition (
FullRepositorycombines all repo capabilities)
┌─────────────────────────────────────────┐
│ UI Layer (AppKit/SwiftUI) │
│ - Window Controllers │
│ - View Controllers │
│ - SwiftUI Views │
└─────────────┬───────────────────────────┘
│
┌─────────────▼───────────────────────────┐
│ Controller Layer │
│ - RepositoryUIController │
│ - GitRepositoryController │
│ - Operation Controllers │
└─────────────┬───────────────────────────┘
│
┌─────────────▼───────────────────────────┐
│ Repository Layer │
│ - XTRepository (libgit2 wrapper) │
│ - Git primitive operations │
│ - File system monitoring │
└──────────────────────────────────────────┘
- Combine publishers broadcast repository changes
- UI components observe state via
@ObservedObjectand publisher subscriptions - File system watchers trigger automatic updates
The foundation layer that wraps libgit2 and provides Swift-native Git operations.
XTRepository
- Core repository class wrapping
OpaquePointer(libgit2 git_repository) - Manages Git repository lifecycle and state
- Thread-safe write operations via
isWritingflag and mutex
Protocol Hierarchy:
FullRepository =
BasicRepository &
Branching &
CommitStorage &
CommitReferencing &
FileDiffing &
FileContents &
FileStaging &
FileStatusDetection &
Merging &
RemoteManagement &
RepoConfiguring &
Stashing &
SubmoduleManagement &
Tagging &
WritingManagement &
WorkspaceEach protocol represents a focused capability:
Branching: Branch creation, deletion, trackingCommitStorage: Commit retrieval and creationFileStaging: Stage/unstage operationsStashing: Stash save/apply/pop/dropRemoteManagement: Remote fetch/push/pull operationsWorkspace: Checkout operations
Why this design?
- Enables precise dependency injection
- Facilitates comprehensive mocking for tests
- Makes capabilities explicit and discoverable
XTRepository.swift- Main repository implementationXTRepository+Commands.swift- Git commands (checkout, stage, etc.)GitODB.swift- Object database accessGitBranch.swift,GitCommit.swift,GitRemote.swift- Git primitivesRepositoryProtocols.swift- Protocol definitions
Mediates between UI and repository, managing state, caching, and async operations.
final class GitRepositoryController: RepositoryController {
let xtRepo: XTRepository
let queue: TaskQueue // Serial queue for repo operations
var cache: RepositoryCache // Cached file changes
// Publishers for repository events
var headPublisher: AnyPublisher<Void, Never>
var indexPublisher: AnyPublisher<Void, Never>
// ...
}Responsibilities:
- Task queue management - Serializes Git operations to prevent conflicts
- Caching - Stores staged/unstaged changes to avoid repeated expensive queries
- File system monitoring - Watches
.git/directory and workspace for changes - Publisher coordination - Broadcasts repository state changes
protocol RepositoryUIController: AnyObject {
var repository: any FullRepository { get }
var repoController: GitRepositoryController! { get }
var selection: (any RepositorySelection)? { get set }
var selectionPublisher: AnyPublisher<RepositorySelection?, Never> { get }
}Implemented by: XTWindowController
Responsibilities:
- Selection management - Current commit/file selection state
- UI coordination - Bridges window controller with repository
- Error display - Shows repository operation errors to user
Encapsulate complex multi-step operations:
StashOperationController- Stash creation dialog → operationResetOpController- Reset confirmation → executionCleanOpController- Clean untracked files dialog
Pattern:
protocol RepositoryOperation {
associatedtype Repository
associatedtype Parameters
var repository: Repository { get }
func perform(using parameters: Parameters) throws
}A hybrid AppKit/SwiftUI architecture leveraging the strengths of both frameworks.
RepoDocument (NSDocument)
└── XTWindowController (main window)
├── SidebarController (branches, remotes, tags)
├── HistoryViewController (commit list)
├── FileViewController (file list + preview)
└── TitleBarController (toolbar)
XTWindowController (Xit/Document/XTWindowController.swift)
- Main window controller implementing
RepositoryUIController - Owns split view layout
- Coordinates child controllers
SidebarController (Xit/Sidebar/SidebarController.swift)
- Shows branches, remotes, tags, stashes, submodules
- Integrates build status (TeamCity)
- Pull request indicators (Bitbucket Server)
HistoryViewController (Xit/HistoryView/)
- Commit history table with graph visualization
- Search/filter capabilities
- Navigation history (back/forward through selections)
FileViewController (Xit/FileView/FileViewController.swift)
- Three-pane file interface:
- Commit list - Files changed in selected commit
- Staging area - Staged vs unstaged changes
- Preview - Diff, blame, text, or QuickLook preview
Modern dialogs and panels use SwiftUI with AppKit interop:
// Pattern: DataModelView protocol
protocol DataModelView: View {
associatedtype Model: ObservableObject, Validating
init(model: Model)
}
// Example: Stash dialog
struct StashPanel: DataModelView {
@ObservedObject var model: StashPanel.Model
var body: some View {
Form {
TextField("Message:", text: $model.message)
Toggle("Keep index", isOn: $model.keepIndex)
}
}
}Bridging: NSHostingController embeds SwiftUI views in AppKit windows
let queue: TaskQueue- Serial dispatch queue for repository operations
- Prevents concurrent writes
- Ensures operation ordering
struct RepositoryCache {
var stagedChanges: [FileChange]?
var amendChanges: [FileChange]?
var unstagedChanges: [FileChange]?
var branches: [String: GitBranch]
}- Caches expensive Git queries
- Invalidated on index/workspace changes
- Thread-safe via
@MutexProtectedproperty wrapper
RepositoryWatcher - Monitors .git/ directory
- Publishes changes to: HEAD, index, reflog, refs, stash
- Uses FSEvents/GCD file monitoring
WorkspaceWatcher - Monitors working directory
- Detects file changes outside of Xit
- Triggers file list refresh
ConfigWatcher - Monitors .git/config
- Reloads repository configuration
- Updates remote/branch tracking info
Abstraction over different states the UI can display:
protocol RepositorySelection {
var repository: any FullRepository { get }
func list(staged: Bool) -> any FileListModel
}Implementations:
CommitSelection- Viewing a historical commitStagingSelection- Current working directory/indexStashSelection- Stash entry contents
Why?
- Unified interface for file lists regardless of source
- Powers file preview across different contexts
// Compose only needed capabilities
typealias Repository =
BasicRepository & CommitStorage & FileStaging- Fine-grained dependency injection
- Clear capability requirements
XTRepository abstracts libgit2, providing:
- Swift-native types (
GitOID,SHA) - Error handling via
throws - Reference counting safety
protocol RepositoryUIAccessor {
var repoUIController: (any RepositoryUIController)? { get }
}
extension RepositoryUIAccessor {
var repoController: (any RepositoryController)?
{ repoUIController?.repoController }
}Used by view controllers to cleanly access repository:
class FileListController: NSViewController,
RepositoryWindowViewController {
// Automatically gets repoUIController, repoController, repoSelection
}struct CheckOutRemoteOperation: RepositoryOperation {
let repository: any Workspace & Branching
let remoteBranch: RemoteBranchRefName
func perform(using parameters: CheckOutRemotePanel.Model) throws {
// Create local branch tracking remote
// Optionally check it out
}
}// Repository broadcasts changes
for await _ in controller.indexPublisher.values {
updateFileList()
}1. User clicks "Stage" button
↓
2. FileListController.stage(_:)
↓
3. repository.stage(file: path)
↓
4. XTRepository.performWriting {
git_index_add_bypath(...)
}
↓
5. WorkspaceWatcher detects .git/index change
↓
6. GitRepositoryController.indexPublisher emits
↓
7. FileViewController observes index publisher
↓
8. UI updates file list (file moved to staged section)
1. User clicks commit in HistoryTableController
↓
2. XTWindowController.select(oid: GitOID)
↓
3. selection = CommitSelection(repository, oid)
↓
4. selectionPublisher emits new selection
↓
5. FileViewController receives selection
↓
6. Loads commit's file list
↓
7. Displays files + diff preview
Xit uses the @Faked macro and manual fake classes for testing:
class FakeRepo: FileChangesRepo &
EmptyCommitReferencing &
EmptyFileDiffing {
var commits: [GitOID: FakeCommit] = [:]
let localBranch1 = FakeLocalBranch(name: "branch1", oid: "a")
func commit(forOID oid: GitOID) -> FakeCommit? {
commits[oid]
}
}"Empty" Protocols:
EmptyCommitReferencing,EmptyFileStaging, etc.- Provide no-op default implementations
- Allow fakes to focus on tested capabilities
XTTest - Base class providing:
- Temporary repository creation/cleanup
- Git command execution helpers
- Common test fixtures
Domain-Specific Languages (DSLs):
The execute(in:) function uses a result builder to simplify the steps needed
to create a repository in the desired state for a test.
try execute(in: repository) {
CommitFiles {
Write("content", to: .file1)
}
CreateBranch("feature")
Stage(.file2)
}- Full integration tests using XCTest UI framework
GitCLIhelper creates repository state via command-line git- Tests complete workflows (commit, branch, stash, etc.)
struct SHA: Hashable {
let bytes: [UInt8] // 20 bytes for SHA-1
}
struct GitOID {
var oid: git_oid // libgit2 C struct
}SHA: Pure Swift value typeGitOID: Wraps libgit2's git_oid for interop
protocol ReferenceName {
var name: String { get }
}
struct LocalBranchRefName: ReferenceName {
let name: String // e.g., "main"
var fullPath: String { "refs/heads/\(name)" }
}
struct RemoteBranchRefName: ReferenceName {
let remote: String // e.g., "origin"
let branch: String // e.g., "main"
}Type-safe branch references prevent mistakes and clarify intent.
struct FileChange {
let path: String
let status: DeltaStatus // .added, .modified, .deleted, etc.
let staged: Bool
}Unified representation of file changes across commits, staging, workspace.
Abstraction over diff generation:
protocol PatchMaker {
func makePatch() -> Patch?
}
enum PatchResult {
case diff(PatchMaker)
case binary
case noDifference
}Handles text diffs, binary files, and unchanged files uniformly.
Xit/
├── Document/ # Window controllers, document management
├── FileView/ # File list and preview components
│ ├── File List/ # File tree/list views
│ └── Previews/ # Diff, blame, text, QuickLook viewers
├── HistoryView/ # Commit history table and graph
├── Operations/ # Multi-step operation controllers
├── Preferences/ # Settings panels
├── Repository/ # Core Git repository abstraction
│ ├── XTRepository.swift
│ ├── RepositoryProtocols.swift
│ └── Git*.swift # libgit2 wrappers
├── Selection/ # Selection abstractions
├── Sidebar/ # Branch/remote/tag sidebar
├── Utils/ # Extensions and helpers
│ ├── Extensions/
│ └── SwiftUI/ # SwiftUI components
└── html/ # HTML templates for diff view
XitTests/ # Unit tests with fakes
XitUITests/ # Integration/UI tests
Xcode-config/ # Build configuration
queue.execute {
// Git operations run serially on background queue
let commit = repository.commit(forSHA: sha)
}UI controllers marked @MainActor:
@MainActor
protocol RepositoryUIController: AnyObject { ... }func performWriting<T>(_ block: () throws -> T) throws -> T {
guard !isWriting else { throw RepoError.alreadyWriting }
isWriting = true
defer { isWriting = false }
return try block()
}Prevents concurrent writes that could corrupt the repository.
-
Define protocol in
RepositoryProtocols.swift:@Faked public protocol MyFeature: AnyObject { func doSomething() throws }
-
Implement in extension:
extension XTRepository: MyFeature { func doSomething() throws { try performWriting { // libgit2 calls } } }
-
Add to FullRepository:
typealias FullRepository = BasicRepository & ... & MyFeature
-
Create fake for testing:
protocol EmptyMyFeature: MyFeature {} extension EmptyMyFeature { func doSomething() throws {} }
- Create SwiftUI view conforming to
DataModelView - Define model conforming to
Validating - Use
SheetDialogprotocol for presentation:struct MyDialog: SheetDialog { typealias ContentView = MyPanel var acceptButtonTitle: UIString { .ok } func createModel() -> MyPanel.Model? { .init() } }
- Xcode project (
Xit.xcodeproj) - Configuration files (
Xcode-config/)Shared.xcconfig- Common settingsDEVELOPMENT_TEAM.xcconfig- Developer-specific (gitignored)
- libgit2 build -
build_libgit2.shscript - Code quality tools:
- SwiftLint (
.swiftlint.yml) - Periphery (
.periphery.yml) - Dead code detection
- SwiftLint (
- Enhanced diff views - Syntax highlighting, better inline diffs
- GitHub integration - Pull requests, fork discovery
- Interactive rebase - Will need new operation controllers
- File history view - Additional selection type
- AppKit → SwiftUI migration - Gradual modernization of views
- Async/await adoption - Replace some Combine publishers
- Memory optimization - Large repository scalability
When contributing to Xit, keep these architecture principles in mind:
- Use protocols for new capabilities
- Provide fake implementations for tests
- Respect thread boundaries - UI on main, repo operations on queue
- Use Combine publishers for state changes
- Follow the accessor pattern for controller access
- Write operations must use
performWriting - New UI panels should use SwiftUI where possible
See CONTRIBUTING.md for detailed development setup.
- libgit2 documentation: https://libgit2.org/docs/
- Git internals: https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain
- Swift Concurrency: https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html
- Combine Framework: https://developer.apple.com/documentation/combine
Last Updated: 2026-01-29 Xit Version: Beta (pre-1.0)