Repository navigation
How Mergetool Mode Works
Nibtree can be the merge tool that git mergetool opens for each conflicted file. In this mode its window shows only the three-pane merge tool, writes the result and tells git through its exit code whether the file is resolved. For the user side, see Git Mergetool.
Many people resolve conflicts from the terminal, or from another tool that calls git mergetool. They should get the same merge tool without opening a repository in the full app. Git's contract for external tools is simple: it passes four file paths, waits for the process to exit, and with trustExitCode it treats exit status 0 as "resolved" and anything else as "not resolved".
Git runs the command from the user's config:
git config --global merge.tool nibtree
git config --global mergetool.nibtree.cmd "\"$APP\" merge \"\$BASE\" \"\$LOCAL\" \"\$REMOTE\" \"\$MERGED\""
git config --global mergetool.nibtree.trustExitCode truesequenceDiagram
participant G as git mergetool
participant RS as Rust main
participant UI as MergeToolApp
participant CMD as commands/merge.rs
G->>RS: nibtree merge BASE LOCAL REMOTE MERGED
RS->>RS: LaunchMode::from_args gives MergeTool
UI->>CMD: getLaunchMode()
CMD-->>UI: mode mergeTool
UI->>CMD: loadMergetool(ignoreWhitespace)
CMD-->>UI: MergeDocument
Note over UI: user resolves in MergeEditor
UI->>CMD: saveMergetool(content, eol)
CMD->>CMD: write MERGED, exit code 0, app.exit(0)
RS->>G: RunEvent::Exit, process exits 0
G->>G: marks the file resolved
LaunchMode::from_args in state.rs reads the command line once at start:
flowchart TD
A["Command line arguments"] --> B["Drop -psn_ arguments that Finder may add"]
B --> C{"First is merge and at least 4 paths follow?"}
C -->|"yes"| D["LaunchMode::MergeTool with base, local, remote, merged"]
C -->|"no"| E{"First argument is not a flag?"}
E -->|"yes"| F["LaunchMode::App with the path,<br/>canonical when possible"]
E -->|"no"| G["LaunchMode::App with no path"]
merge with fewer than four paths is not mergetool mode, so a typo opens the normal app instead of a broken merge window. (nibtree cli ... is caught even earlier, in lib.rs, and never opens a window; see How MCP and CLI Work.) AppState stores the mode and a mergetool_exit_code (AtomicI32) that starts at 1.
On the frontend, App.svelte calls api.getLaunchMode() after settings.init(). For mode: "mergeTool" it renders MergeToolApp.svelte and nothing else: no workspace, no watcher, no sidebars. The settings still load; the update store sees the mergetool mode and schedules no update check and shows no What's New. appMenu.install("mergeTool") builds a short menu bar from menuSpec (on macOS: Nibtree with Settings, Edit, Window and Help), and the MCP server, memory log and other main window services start only when the mode is app, so a mergetool window never competes for the MCP port.
load_mergetool calls conflicts::load_files(base, local, remote, merged, ignore_whitespace). It reads the three input files directly from the paths git gave, not from a repository index. An empty or missing BASE means both sides added the file, so the kind is BothAdded. The panes are labeled "Local (yours)" and "Remote (theirs)". The result goes through the same build_document as an in-repo conflict, so the chunks, conflicts and binary check are identical. A binary file shows a notice with a Quit button. Any input file that cannot be read loads as empty text, not only BASE.
MergeToolApp sets the OS window title to Merge <file name> (the in-window title bar reads "Merge Revisions for" and the file) and passes the document to MergeEditor.svelte, the same component the full app uses. Toggling "ignore whitespace" saves the setting and reloads the document.
stateDiagram-v2
[*] --> Unresolved: start, exit code 1
Unresolved --> Saved: Apply, MERGED written, code 0
Unresolved --> Cancelled: Cancel, Quit or closing the window, code 1
Unresolved --> Closed: Cmd+Q, code stays 1
Saved --> [*]: process exits 0
Cancelled --> [*]: process exits 1
Closed --> [*]: process exits 1
-
save_mergetool(content, eol)writes MERGED with the document's line endings (eol.apply), stores 0 and callsapp.exit(0). -
cancel_mergetoolstores 1 and callsapp.exit(1). - In
lib.rs, theRunEvent::Exithandler callsstd::process::exitwith the stored code whenever the app runs as a mergetool.
That last step is what makes quitting safe. The red close button goes through Cancel (onCloseRequested in MergeToolApp), but Cmd+Q never calls our commands, so without the handler the process would exit normally. The stored code is the single source of truth, and it only becomes 0 after an explicit save.
| File | What it does |
|---|---|
src-tauri/src/state.rs |
LaunchMode, from_args, AppState.mergetool_exit_code
|
src-tauri/src/lib.rs |
RunEvent::Exit handler that exits with the stored code |
src-tauri/src/commands/merge.rs |
load_mergetool, save_mergetool, cancel_mergetool
|
src-tauri/src/commands/repo.rs |
get_launch_mode |
src-tauri/src/git/conflicts.rs |
load_files, shared build_document
|
src/lib/App.svelte |
Picks MergeToolApp for the mergetool mode |
src/lib/merge/MergeToolApp.svelte |
The standalone window: load, apply, cancel, the close request |
src/lib/merge/MergeEditor.svelte, mergeSession.svelte.ts
|
The three-pane merge tool, shared with the full app |
Exit code as the only signal. This is git's contract, so no files, sockets or flags are needed. Starting at 1 means every path except a real save leaves the file unresolved, which is the safe default.
Write MERGED directly, do not stage. In the full app, save_resolution writes the file and then stages it with git add. Here git itself runs the tool and stages the file when we exit with 0, so staging it ourselves would only do the same work twice.
Same merge engine and editor. A second, lighter merge UI would drift from the main one. Reusing build_document and MergeEditor means a fix in one place helps both.
Read the command line in Rust. The mode is known before any web view loads, and the frontend only asks for it, so the arguments are parsed and tested in one place.
Closing the window dropped your work.
- The issue: in git mergetool mode, closing the window with the red button threw away an edited result without asking, although Cancel and Esc asked first.
- Why it happened: nothing listened for the close request, so the window closed and the app exited with the unresolved code.
-
The fix and why we chose it:
MergeToolApplistens withonCloseRequested, always keeps the window open, and runs the same cancel as the Cancel button. If you confirm,cancel_mergetoolexits with code 1 as before. We never destroy the window from the frontend, so no extra window permission is needed.
Update checks ran in mergetool mode.
- The issue: every window git mergetool opened for a file also started the timed update check, wasted work in a window that lives for one file.
-
Why it happened:
updates.init()ran the same way in both launch modes. -
The fix and why we chose it:
updates.init()asksisMergetoolLaunch()and skips the check, and leaveslastRunVersionalone so the next normal start still shows What's New. The full app still checks on its next start; see How Updates Work.
Renames to know about: from Git Merger to Git Manager the tool name became gitmanager (binary git-manager), and from Git Manager to Nibtree it became nibtree (binary nibtree, in Nibtree.app). An old gitmerger or gitmanager config must be set up again with the new lines.
-
src-tauri/src/state.rs:merge_arguments_select_mergetool_mode(also with a-psn_argument),incomplete_merge_arguments_are_not_mergetool_mode,repo_path_argument_is_canonicalized,no_args_flags_and_psn_open_nothing. -
src-tauri/src/git/tests.rs:load_files_with_empty_base_is_both_added. - The merge logic itself is covered by the merge tests, see How the Merge Tool Works.
save_mergetool and the exit handler are not unit tested, because they end the process. To check them by hand, run the built binary with merge and four sample files and echo $? after Apply, after closing the window and after Cmd+Q. A pure test for writing MERGED with the right line endings would be a good addition.
- Update this page when
LaunchMode, the mergetool commands or the exit handler change. - Update Git Mergetool and the README when the command line or tool name changes.
- Retake
mergetool-mode.pngwhen the standalone window looks different. - Related: How Conflict Resolution Works, Backend.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode