Terminal-based merge conflict resolver written in Fortran
fit is a modern terminal-based tool for resolving Git merge conflicts. Features a three-pane TUI with side-by-side incoming/local comparison and live preview of the resolved file. Supports both interactive resolution and automated CI/CD workflows with command-line flags.
fit is available for Fedora/RHEL, Arch Linux (AUR), macOS (Homebrew), and can be built from source using Make or the Fortran Package Manager (fpm).
Open a file with merge conflicts to start resolving:
Configure fit as your default merge tool:
| Option | Type | Default | Description |
|---|---|---|---|
fit <file> | command | - | Open file in interactive TUI mode |
--incoming, -i | flag | - | Auto-resolve all conflicts with incoming (remote) version |
--local, -l | flag | - | Auto-resolve all conflicts with local (current) version |
--both, -b | flag | - | Auto-resolve all conflicts with both versions combined |
0 - Success (file resolved and saved)1 - Error (file not found, parse error, or quit without saving)
Configure fit as your default merge tool by adding to ~/.gitconfig:
fit does not use environment variables. Terminal dimensions are auto-detected
via stty size. ANSI color support is assumed (256-color terminal
recommended).
Tested on: Linux terminals (xterm, GNOME Terminal, Konsole), macOS Terminal, iTerm2, SSH sessions, tmux, and screen.
fit is a terminal-based merge conflict resolver designed as a lightweight alternative to vimdiff and meld. It provides an intuitive three-pane interface for visual side-by-side conflict comparison and interactive resolution.
The TUI displays conflicts in a split-screen layout:
┌───────────────────────────────────────────────────────┐
│ INCOMING │ LOCAL │
│ (from remote/HEAD) │ (current branch) │
├───────────────────────────────────────────────────────┤
│ PREVIEW - Live resolution showing resolved file │
│ │
├───────────────────────────────────────────────────────┤
│ [i]ncoming [l]ocal [b]oth | [n]ext [p]rev | [s]ave │
└───────────────────────────────────────────────────────┘ Use arrow keys to switch between panes and scroll within them:
← / → - Switch to previous/next pane↑ / ↓ - Scroll within active pane
The active pane is highlighted with a yellow border and ◀ indicator.
Each pane has independent scrolling.
+ prefix on conflict lines- prefix on conflict lines
Pressing i selects the incoming (remote/HEAD) version:
<<<<<<< and ============== and >>>>>>>Use when the remote/pulled branch has the correct implementation.
Pressing l selects the local (current branch) version:
<<<<<<< and ============== and >>>>>>>Use when your current branch has the correct implementation.
Pressing b keeps both versions:
Use when combining features from both sides. Note: This may create duplicates and requires manual review.
| Feature | fit | vimdiff | meld |
|---|---|---|---|
| Terminal-based | Yes | Yes | No |
| Three-pane view | Yes | Yes | Yes |
| Auto-resolve flags | Yes | No | No |
| Simple keybindings | Yes | Complex | N/A |
| Binary size | ~2MB | Large | Large |
| No X11 required | Yes | Yes | No |
fit is implemented in approximately 3,500 lines of modern Fortran across 6 specialized modules. The architecture follows a clean separation of concerns: terminal control, conflict parsing, resolution logic, and TUI rendering.
fit/
├── app/
│ └── main.f90 # Entry point, event loop
├── src/
│ ├── terminal_control.f90 # ANSI terminal control
│ ├── pane_state.f90 # Pane state and scrolling
│ ├── conflict_parser.f90 # Parse conflict markers
│ ├── resolution_engine.f90 # Write resolved files
│ ├── keyboard_input.f90 # Key detection
│ └── tui_layout.f90 # Three-pane rendering
└── test/ # Test conflict files
The terminal_control.f90 module handles ANSI terminal manipulation:
stty size
The conflict_parser.f90 module extracts conflict data:
type conflict_t
integer :: start_line, middle_line, end_line
character(len=:), allocatable :: incoming_branch
character(len=:), allocatable :: local_branch
character(len=:), allocatable :: incoming_lines(:)
character(len=:), allocatable :: local_lines(:)
character(len=:), allocatable :: context_before(:)
character(len=:), allocatable :: context_after(:)
integer :: choice ! 0=none, 1=incoming, 2=local, 3=both
end type Uses two-pass parsing: first reads all lines, then identifies conflict markers and extracts components with 5 lines of context.
The resolution_engine.f90 module writes resolved files:
Input File
↓
┌─────────────────────────────────┐
│ conflict_parser │
│ Parse markers → conflict_t[] │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Non-interactive mode? │
│ → Set choices, write, exit │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Event Loop │
│ 1. tui_layout: Draw panes │
│ 2. keyboard_input: Wait for key │
│ 3. Process: Update choice/nav │
│ 4. Loop │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ resolution_engine │
│ Write resolved file │
└─────────────────────────────────┘ The parser recognizes standard Git conflict markers:
<<<<<<< - Conflict start (7 characters)======= - Separator (7 characters)>>>>>>> - Conflict end (7 characters)State machine: Normal → Start → Middle → Normal, extracting branch names from the markers.
| Option | Type | Default | Description |
|---|---|---|---|
i | key | - | Select incoming (remote/HEAD) version |
l | key | - | Select local (current branch) version |
b | key | - | Select both versions (incoming then local) |
| Option | Type | Default | Description |
|---|---|---|---|
n | key | - | Go to next conflict |
p | key | - | Go to previous conflict |
Arrow Left | key | - | Switch to previous pane |
Arrow Right | key | - | Switch to next pane |
Arrow Up | key | - | Scroll up in active pane |
Arrow Down | key | - | Scroll down in active pane |
| Option | Type | Default | Description |
|---|---|---|---|
s | key | - | Save resolved file and exit |
q | key | - | Quit without saving |
ESC | key | - | Same as q - quit without saving |
The bottom status bar shows:
[i/l/b] choose | [←→] pane | [↑↓] scroll | [n/p] conflict | [s]ave [q]uit (1/5) The right side shows the current conflict number (e.g., "1/5" = first of 5 conflicts).
Cause: Terminal doesn't support ANSI colors or incorrect TERM variable.
Cause: File has no conflict markers or markers are malformed.
Cause: Some conflicts haven't been assigned a resolution.
Cause: Running in a container or SSH without proper TTY.