javascript-today

My Daily Magit and Ediff Workflow

I’ve mentioned in a couple of other posts that Magit and ediff ended up being the two Doom Emacs pieces I use the most, and the ones that surprised me most. This post is less “here’s a keybinding table” and more “here’s the actual sequence of keys I press, in order, for the git work I do every day.”

The morning status check

First thing every day, SPC g g opens Magit status. This one buffer replaces git status, git diff, git log --oneline -10, and git branch as four separate terminal commands — they’re all sections in the same view, and every section is foldable with TAB.

I keep the log section (usually collapsed) expanded just enough to see the last handful of commits so I remember where I left off yesterday, and the “Unstaged changes” section is what I actually work from. Pressing RET on any file under it jumps straight into a diff of that file inline, in the same buffer — no new window, no context switch.

Staging: hunks and lines, not files

This is the habit that took the longest to build and paid off the most. In a terminal git workflow, git add -p gets you hunk-level staging, but it’s clunky — you’re answering y/n/s prompts one at a time with limited visibility into the surrounding code.

In Magit, I navigate into a file’s diff with TAB, put point on the hunk I want, and press s to stage just that hunk. If a hunk mixes an intentional change with something incidental (a stray console.log, a formatting fix I don’t want bundled into this commit), I go a level finer: put point on the actual line, use the region to select a few lines, and s stages exactly that selection. u does the same thing in reverse for unstaging.

The result is that my commits are almost never “everything I happened to touch today” — they’re deliberately scoped, because scoping them costs nothing extra over staging everything at once.

Committing

c c from the status buffer opens a dedicated commit message buffer — a real buffer with syntax highlighting (the summary line goes red past 50 characters, which is a genuinely useful nag), not a single-line terminal prompt. I write the message, C-c C-c to confirm, and I’m back in the status buffer with the commit already reflected in the log section.

If I mistype something and haven’t pushed yet, c e on the top commit reopens it for amending using the same buffer.

Reviewing before I commit: this is where ediff comes in

Before I stage anything for a change bigger than a couple of lines, I want to see exactly what changed without the visual noise of a unified diff’s +/- prefixes cluttering every line. From the status buffer, putting point on a file and pressing e (magit-ediff-dwim) drops me into ediff comparing the working file against HEAD.

The actual sequence I use constantly:

  • n / p to step forward/backward through hunks — this is faster than scrolling, because it jumps to the next change, not the next screen.
  • If I spot something I want to fix while reviewing (a leftover debug line, a typo), I just edit the buffer right there — ediff isn’t a read-only viewer, it’s a live buffer. Then ! recomputes the diff so the hunk list updates to match.
  • q to quit once I’ve confirmed the change looks right, back to Magit to stage and commit.

This “review and fix in the same pass” loop is the actual daily value — I’m not tabbing between a diff viewer and my editor, they’re the same window.

Merge conflicts: three-way ediff instead of reading markers

This is where ediff stops being a nice-to-have and becomes something I’d genuinely miss. When a rebase or merge leaves a file conflicted, Magit’s status buffer shows it under “Unmerged changes.” Pointing at it and pressing E m (magit-ediff-resolve-rest, which leaves any conflicts you’ve already resolved by hand alone) or E M (magit-ediff-resolve-all, which reconstructs the file fresh from both sides plus the merge base) opens Ediff with your side, their side, and the in-progress merge result as separate buffers:

  • A — my side (the branch I’m on)
  • B — their side (the branch being merged in)
  • merged — the actual output buffer I’m editing

Instead of scanning <<<<<<</=======/>>>>>>> markers and manually deleting the ones I don’t want, I use n/p to move hunk by hunk, and at each conflicting hunk: a copies A’s version into the merge buffer, b copies B’s version, or I just type the resolution by hand if it’s neither. Once every hunk is resolved, q quits Ediff — it prompts to save the merged buffer back out to the file — and I flip back to Magit to stage the now-resolved file with s and commit like normal.

The three-way view is the part that actually matters here — seeing “your side” and “their side” as two real buffers side by side, rather than inline markers in one buffer, makes it obvious why two changes collided, not just that they did.

Branches and pushing

Once everything’s committed, b b checks out a branch (with completion, so I’m typing a few characters and hitting RET, not copy-pasting a branch name), and P p pushes to the current branch’s upstream. If I need to see what a colleague’s branch actually changed before reviewing a PR, E r (magit-ediff-compare) prompts for two revisions — usually main and their branch — shows me which files differ, and lets me step into ediff on each one with the same hunk-by-hunk navigation as above.

Why this replaced my terminal git habit

None of these are individually things git on the command line can’t do — add -p, diff, mergetool all exist. What changed is that staging, diffing, resolving, and committing all live in the same keyboard-driven buffer with the same conventions, so there’s no context switch between “the tool I’m editing code in” and “the tool I’m managing git in.” That’s the thing I didn’t expect going in, and it’s the reason Magit plus ediff is the part of this whole setup I’d be most reluctant to give up.

Quick reference

Everything above, collapsed into the keys I actually reach for.

Magit status buffer (opened with SPC g g):

Key Action
TAB Expand/collapse a section, or a file’s inline diff
RET Visit the thing at point (file, hunk, commit, conflicted file)
s Stage the hunk/line-selection/file at point
u Unstage the hunk/line-selection/file at point
c c Open the commit message buffer
c e Amend the top commit
e magit-ediff-dwim — compare or resolve the diff/conflict at point
E Open the full Ediff menu (E r compare revisions/branches, E m/E M resolve conflicts)
b b Checkout a branch
P p Push to upstream
F p Pull from upstream
g r Refresh the status buffer
q Quit back out of Magit

Inside ediff (comparing working tree vs. HEAD, or two branches):

Key Action
n / p Jump to the next/previous diff hunk
! Recompute the diff after editing a buffer live
q Quit ediff

Inside ediff-merge (three-way conflict resolution):

Key Action
n / p Jump to the next/previous conflicting hunk
a Take buffer A’s (your side’s) version of this hunk
b Take buffer B’s (their side’s) version of this hunk
q Quit — prompts to save the merged result back to the file

Comparing branches without a full checkout (from Magit, E r):

Command Action
magit-ediff-compare Prompts for two revisions/branches, lists changed files, ediffs the one you pick