Every developer has a merge conflict horror story. Mine usually starts with vimdiff opening four panes of identical-looking code in a terminal window too small to make sense of any of them. I hit :diffget, pray I picked the right buffer number, and repeat until the conflict markers disappear. It works. It has never once felt good.
I've tried the GUI tools. Meld, Beyond Compare, VS Code's built-in merge editor. They're fine if you live in a desktop environment, but I spend most of my day in a terminal over SSH. What I wanted was a merge tool that understood git's 3-way conflict model, ran in the terminal, and didn't make me memorize arcane buffer commands just to pick "theirs."
So I went looking, and I found one that does exactly that.
What Is ec?
ec is a terminal-native 3-way git mergetool written in Go by chojs23. You point it at a file with merge conflicts (or just run ec with no arguments and let it find them) and it opens a TUI with three side-by-side panes: yours, theirs, and the base. You navigate between conflicts with j/k, pick a resolution with a single keystroke, and write the result. That's it.
It plugs directly into git mergetool as a drop-in replacement, but it also works standalone. Run ec in a repo with unresolved conflicts and it presents a file picker, then walks you through each conflict one at a time. Undo, redo, and manual editing are all built in. The whole thing is about 9,000 lines of Go.
Under 150 stars. For a tool this useful, that's criminal.
The Snapshot
| Project | ec |
| Stars | ~142 at time of writing |
| Maintainer | Solo developer, actively committing |
| Code health | Clean, well-structured, properly tested |
| Docs | README covers installation and usage; internals are self-documenting |
| Contributor UX | Small codebase, clear package boundaries, easy to navigate |
| Worth using | Yes, already replaced vimdiff in my config |
Under the Hood
ec is built on the charmbracelet stack: bubbletea for the TUI framework, bubbles for viewport widgets, lipgloss for styling. If you've used any charmbracelet project, the Elm-style architecture (Model / Update / View) will be immediately familiar. If you haven't, ec is a pretty clean example of how it works.
The package layout is where things get interesting. There are seven internal packages and they have clear responsibilities:
-
markershandles parsing and rendering conflict markers, including the diff3 base section -
enginemanages resolution state with full undo/redo history -
gitmergewrapsgit merge-file --diff3to regenerate three-way diffs from the index -
tuiis the bubbletea model that ties everything together
The parser in markers is particularly solid. It handles the full diff3 format (ours, base, and theirs sections with their labels) and produces a clean document model of alternating text and conflict segments. The engine builds on top of that, tracking which conflicts are resolved, which resolution was picked, and maintaining a state stack for undo.
What I appreciated most is what's not here. There's no framework bloat, no dependency soup, no abstraction astronautics. The go.sum is small. The packages do one thing each. The code reads like someone who writes Go because they like Go, not because they needed a language and picked one.
The rough edges are minor. Documentation is README-only, with no --help beyond flag descriptions. There's one open bug around cursor position after undo (#11) that the maintainer has acknowledged. The test coverage is decent for the core logic but thin on the TUI layer, which is common with bubbletea projects since testing view rendering is awkward.
None of that matters much at this stage. The tool works, the code is clean, and the architecture will scale if the project grows.
The Contribution
I wanted to contribute something small but visible, so I looked at the TUI headers. When you're resolving a conflict, the three panes are labeled "OURS", "BASE", and "THEIRS". Functional, but not helpful when you're five conflicts deep and can't remember which branch is which. The code had a formatLabel function that was supposed to show branch names, but it was stubbed out to return an empty string. I figured it was unfinished and started implementing it.
Then I tested it and saw the pane headers display OURS (/tmp/ec-local-4183502281).
That's when I understood why it was stubbed. ec extracts the three versions of a conflicted file to temp files, then runs git merge-file --diff3 to produce a clean three-way diff. But merge-file uses filenames as conflict marker labels, so the resulting diff3 document has temp paths baked into its labels. The formatLabel function wasn't unfinished. It was deliberately disabled because the data feeding it was garbage.
The fix was to go upstream of the problem. The original merged file — the one sitting in the working tree with conflict markers from git itself — has the real labels: branch names, commit hashes, whatever git wrote during the merge. I added a function that extracts those labels before any resolution logic touches the document, stores them on the TUI model separately, and reads them in the view renderer. This keeps the labels stable through undo, redo, and reload operations, which all mutate the document.
The result: OURS (HEAD) and THEIRS (feature/add-auth) during a merge, THEIRS (2367582 (main work)) during a rebase with long hashes truncated for readability, and a clean fallback to plain OURS / THEIRS when labels aren't available.
Getting into the codebase took about an hour. The package boundaries made navigation straightforward: labels lived in markers, state lived in engine, display lived in tui. The contribution touched two files and added 14 tests. PR #12 was merged the next day. The maintainer spotted an edge case I'd missed: if you reopen a partially resolved merge where some conflicts were already resolved, the label indices shift. He fixed it himself and merged the whole thing. That's a good sign for the project.
The Verdict
ec is for developers who live in the terminal and are tired of fighting their merge tool. If you use vimdiff because it's the default and you never got around to finding something better, this is that something better. If you work over SSH and the GUI tools aren't an option, this fills the gap.
The project is early and solo-maintained, which means it could stall. But the commit history suggests an active developer who's iterating. Recent merges include fixes for edge cases like missing base chunks, which tells me the maintainer is actually using this tool on real merge conflicts, not just demoing it.
What would push ec to the next level? Better discoverability, for one. The README could use a GIF showing the workflow, and a Homebrew formula or prebuilt binaries would lower the installation barrier. A plugin system for custom keybindings would appeal to the vim crowd. But honestly, the core is already solid. It just needs people to find it.
Go Look At This
If you resolve merge conflicts in a terminal, go try ec. Install it, set it as your mergetool, and pick a fight with a conflicting branch. You'll know within two conflicts whether it fits your workflow.
Star the repo. Open an issue if something breaks. If you want to contribute, the codebase is small enough to read in an afternoon, and that's how I ended up getting a PR merged.
This is Review Bomb #1, a series where I find under-the-radar projects on GitHub, read the code, contribute something, and write it up. If you know a project that deserves more eyeballs, drop it in the comments.
Top comments (0)