Using Ediff in 2023
10 Jul 2023 Charles Choi
I confess, it took me a long time to really be comfortable working with diffs, especially when rendered on top of each other. Back in the late 90's TkDiff changed that for me because it would show them side-by-side. At the time it seemed like an impressive feat. Ediff could do it too and it first came out in 1995. And yet…
For many years I’ve known about Ediff but whenever I got around to using it, my end emotional state would be: “wow that’s awkward.” Ultimately what I wanted to do with Ediff was to see the diffs for a modified repo file side-to-side with its checked in counterpart just like in TkDiff. And I wanted to do it right from the current buffer. Seems straightforward, yes? Not quite. Let’s go through an example.
On GUI Emacs 28.2 with Ediff defaults, invoking M-x ediff-revision
on a modified repo file will:
- Prompt you for the first file (default is the file in the buffer)
- Prompt you for a branch containing a variant of the first file (default is the current branch)
- No completion support is provided here.
- Open a new frame (GUI window) holding the controls (next, previous, copy left, copy right, etc.) which must be in focus to receive your keyboard event.
- Create two Emacs windows1, one on top of the other
- At this point you are free to navigate the diffs between the two files.
- Quitting in the control frame will:
- Delete the control frame.
- Keep the two buffers in place (because the variable
ediff-keep-variants
default value ist
).
Worse yet, if you set ediff-keep-variants
to nil
to dispose of them, you would be prompted each time before killing the aforementioned two buffers, regardless if there is no change to them.
This is what I mean by awkward. Contemporary IDEs (examples: anything from JetBrains, Xcode, VSCode, Eclipse) won’t make you go through anything near that to diff a modified repo file.
Why I think Ediff goes through such hoops is because it was designed to be profligate in creating state and timid in destroying it. I wouldn’t in the least be surprised if its original design intent was to only diff and merge two arbitrary files and that support for revision control was tacked on later in life (recall, Ediff was released in 1995!).
So why work with Ediff? My personal answer is that I’m too invested in Emacs. I want a similar solution provided by contemporary IDEs and Ediff is the only package I know of that provides side-by-side and merge support. If I want to stay in Emacs and I don’t want to write a new package, Ediff is the only horse to ride on.
Thus began my journey delving into the internals of Ediff to make a contemporary diff workflow for a modified repo file.
I got it to work. Here's a demo video (click on image) calling Ediff from the minibuffer with the function cc/ediff-revision
. A similar demo using the mouse and context-menu-mode
is shown here.
Feels smoother, no? But to get this, I had to make a Devil’s bargain. Still here? Read on.
Ediff Variable Setup
Setup the following Ediff variables as shown in the table below:
Variable | Setting | Notes |
---|---|---|
ediff-keep-variants | nil | Kill variants upon quitting an Ediff session |
ediff-split-window-function | split-window-horizontally | Show diffs side-by-side |
ediff-window-setup-function | ediff-setup-windows-plain | Puts the control panel int same frame as the diff windows1 |
A Nasty Surprise
In my earlier post “Surprise and Emacs Defaults”, I described how to restore the window1 configuration in ediff-quit-hook
that was taken from the Stack Exchange post helm - Restoring windows and layout after an Ediff session. There is a problem though: the suggested ediff-quit-hook
addition will break the logic Ediff uses to kill ancillary buffers (for example ✷ediff-errors✷
, ✷ediff-diff✷
, ✷ediff-find-diff✷
, and the diff control panel) upon quitting, particularly when ediff-keep-variants
is set to nil
. Again I’d observe that Ediff is quite profligate about creating state. If you are using the functions my-store-pre-ediff-winconfig
and my-restore-pre-ediff-winconfig
referenced in my earlier post, I recommend you replace that code with what I describe below.
Ediff Makeover: Support Diffing a Modified Repo File
To help clarify/sanity-check what I wanted to achieve with Ediff, I wrote a mini-PRD below.
User Story/Workflow
User has opened in the current buffer a file that is checked-in to a repository. User has modified that buffer and now wants to see the difference between said modifications and its checked-in counterpart in the current branch.
Requirements
- The user shall invoke above workflow using either a keyboard (M-x) or a mouse on a current buffer holding the version controlled file.
- If the buffer has not been saved to disk, the user must be prompted to save it before the diff operation proceeds.
- The user must not be prompted to select the version controlled file nor its current branch.
- If the file is not version controlled, no diff operation shall be made. A warning message shall be provided.
- If the buffer has no file associated with it, no diff operation shall be made. A warning message shall be provided.
- The diffs shall be displayed in accordance with ediff-split-window-function.
- The window1 configuration must be checkpointed such that it can be restored after the diff session is completed.
- Every workflow interaction must be in the same frame1.
- Code changes to Ediff functions are to not have adverse side-effects on Ediff behavior that is unrelated to the above workflow.
Implementation
The following shows all the code annotated with comments to support the above requirements. Note that the original Ediff function ediff-janitor
is overridden to keep around the buffer holding the modified version controlled file.
Since a window1 configuration can be stored in an Emacs register, I use that to checkpoint and restore it, using a register identifier (🧊) that is unlikely to be typed in from a keyboard during interactive use (one less global variable!).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
|
With the above code loaded in your Emacs configuration, you can invoke cc/ediff-revision
to your hearts content.
Summary
A common workflow in contemporary IDEs is to show the differences between a modified repo file and its checked-in counterpart. This post shows how you can do this in Emacs with Ediff, but at the cost of overriding ediff-janitor
, being exposed to risk if its original behavior is ever changed. Try it out and let me know on Mastodon (Charles Choi 최 민수 (@kickingvegas@sfba.social) - SFBA.social) how well it works for you. Thanks for reading!
Acknowledgements
- Thanks to q01 on
#emacs
IRC for identifyingediff-janitor
as the starting point to achieve what I wanted from Ediff. - Thanks to all who participated in my informal poll of Ediff usage on Mastodon. Unsurprisingly, not many folk actually use Ediff to address the use-case described in this post, arguably because:
References
- helm - Restoring windows and layout after an Ediff session - Emacs Stack Exchange
- How can I quit ediff immediately without having to type 'y' - Emacs Stack Exchange
- ediff via magit leaves ediff-related "mess" buffers after exiting · Issue #1934 · magit/magit
- Setting up Ediff · (or emacs
- How to kill ediff's buffers on quit? - Emacs Stack Exchange
- Quickly ediff files from dired · (or emacs
Footnotes
1 Emacs UI definition