GitFlow is a macOS Git GUI application built with Swift and SwiftUI, following the MVVM (Model-View-ViewModel) architecture pattern. The application uses the system's Git CLI for all Git operations, ensuring full compatibility with user configurations and hooks.
┌─────────────────────────────────────────┐
│ SwiftUI Views │
│ (@StateObject/@Published bindings) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ ViewModels │
│ (Business logic, async Git ops) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Services │
│ (Git CLI wrapper, parsers) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Models │
│ (Pure data structures) │
└─────────────────────────────────────────┘
Pure value types representing Git entities. These are immutable, thread-safe, and contain no business logic.
Key Models:
Repository: Represents a Git repository with its root URLBranch: Local or remote branch with tracking informationCommit: Commit with full metadata (author, date, message, parents)FileStatus: File status in the working tree (staged/unstaged/untracked)FileDiff: Diff for a single file with hunks and linesDiffHunk: A contiguous block of changesDiffLine: A single line in a diff with type and line numbersGitError: Typed errors for Git operations
Handles Git command execution and output parsing.
Components:
- Async Process wrapper for executing Git commands
- Handles timeouts, environment setup, and output capture
- Thread-safe via Swift actor
- High-level facade for Git operations
- Composes commands and parsers
- Provides typed async methods for all Git operations
Protocol-based command pattern for Git operations:
StatusCommand: Working tree statusDiffCommand: File diffs (staged, unstaged, commit)LogCommand: Commit historyBranchCommand: Branch listing and operationsCommitCommand: Commit creationStageCommand: Staging/unstaging filesRemoteCommand: Remote management (add, remove, rename, set-url)
Parse Git CLI output into model objects:
StatusParser: Parsesgit status --porcelainoutputDiffParser: Parses unified diff formatLogParser: Parses custom log format with field separatorsBranchParser: Parses branch list with tracking info
ObservableObjects that manage state and coordinate between views and services.
Key ViewModels:
- Main coordinator for a repository
- Manages child ViewModels
- Handles cross-cutting concerns
- Working tree status management
- File selection and batch operations
- Stage/unstage operations
- Diff loading and display
- View mode switching (unified/split)
- Display settings
- Commit message composition
- Validation and guidelines
- Commit creation
- Commit history loading
- Pagination support
- Filtering by branch/file
- Branch listing
- Checkout operations
- Create/delete branches
SwiftUI views that render the UI and bind to ViewModels.
Organization:
Main/: Primary window structure (MainWindow, Sidebar, ContentArea)Repository/: Repository-level viewsStatus/: Working tree status viewsDiff/: Diff visualization (Unified, Split)Commit/: History and commit creationBranch/: Branch managementShared/: Reusable components (LoadingView, ErrorView, ConfirmationDialog, AvatarView)
Shared Components:
AvatarView: Displays user avatars in commit history. Uses Gravatar service to fetch images based on email hash, with an initials-based fallback showing colored circles with user initials for users without Gravatar accounts.
1. User opens repository
2. AppState.openRepository() called
3. GitService.isGitRepository() validates path
4. RepositoryViewModel created with GitService
5. RepositoryViewModel.refresh() loads all data
6. Child ViewModels update their @Published state
7. SwiftUI views react to state changes
1. User right-clicks file, selects "Stage"
2. StatusViewModel.stageFiles([path]) called
3. GitService.stage(files:in:) executes command
4. StatusViewModel.refresh() reloads status
5. Status published, views update
1. User selects file in list
2. StatusViewModel.selectedFile updated
3. Binding propagates to DiffViewModel.loadDiff()
4. GitService.getStagedDiff() or getUnstagedDiff() called
5. DiffParser.parse() converts output to FileDiff
6. DiffViewModel.currentDiff updated
7. DiffView renders hunks and lines
- All Git operations are async using Swift concurrency
- GitExecutor is an actor for thread-safe command execution
- ViewModels are @MainActor for UI-safe state updates
- Long operations show loading indicators via @Published state
Errors flow through the stack as typed GitError values:
- GitExecutor throws on command failure
- GitService methods propagate or wrap errors
- ViewModels catch and expose via @Published error property
- Views display errors via alerts or inline messages
RecentRepositoriesStore: Stores recently opened repos in UserDefaultsSettingsStore: Stores user preferences (diff mode, font size, etc.)
- Parser tests with fixture files containing real Git output
- ViewModel tests with mock GitService
- Model tests for edge cases
- End-to-end tests with real Git repositories
- Command execution verification
- Edge cases: empty repo, merge conflicts, binary files
- Large repos with many files
- Various Git configurations
The architecture supports extension via:
- New GitCommand types for additional Git operations
- New ViewModels for additional features
- New parsers for different Git output formats
- Settings for user customization
- No credentials are stored by the application
- Git operations use user's existing Git configuration
- Terminal prompts are disabled to prevent hangs
- File paths are validated before operations