difftastic is a structural diff tool that compares files based on their syntax. So for example, if you conditionalize some statement, the diff would only show the addition of one if with its condition instead of showing one line added (the if (condition)) and the line with the statement being removed and re-added (because of indentation changes). In many cases, such structural diffs transport the meaning of a change much better than the typical line-based diffs.

Of course, that comes with a price: difftastic has to understand the language's syntax (it currently supports these languages) and computing a structural diff is a quite expensive operation. Also, there are certain kinds of changes where a line-based diff with changed-word highlighting gives better results, namely when the changed syntactic unit is a large blob with no sub-structure, e.g., a docstring or a comment.

Git allows to use an external diff tool in many commands by setting the environment variable GIT_EXTERNAL_DIFF=<tool> and in the following I make use of that for being able to use difftastic for git show and git diff when operating on a git repository from inside Emacs using Magit. Because the aforementioned downsides, I want that as opt-in behavior, i.e., separate commands. Also, Magit has some assumptions on how a git diff looks like which are not met by difftastic, e.g., difftastic prints line numbers and generates side-by-side diffs for changes which are not plain additions or deletions.

So here we go. Let's first define a helper function which sets GIT_EXTERNAL_DIFF to difft (the difftastic binary), runs the given git command in a new process asynchronously, and pops up a buffer with the result once the process finished.

(defun th/magit--with-difftastic (buffer command)
  "Run COMMAND with GIT_EXTERNAL_DIFF=difft then show result in BUFFER."
  (let ((process-environment
         (cons (concat "GIT_EXTERNAL_DIFF=difft --width="
                       (number-to-string (frame-width)))
               process-environment)))
    ;; Clear the result buffer (we might regenerate a diff, e.g., for
    ;; the current changes in our working directory).
    (with-current-buffer buffer
      (setq buffer-read-only nil)
      (erase-buffer))
    ;; Now spawn a process calling the git COMMAND.
    (make-process
     :name (buffer-name buffer)
     :buffer buffer
     :command command
     ;; Don't query for running processes when emacs is quit.
     :noquery t
     ;; Show the result buffer once the process has finished.
     :sentinel (lambda (proc event)
                 (when (eq (process-status proc) 'exit)
                   (with-current-buffer (process-buffer proc)
                     (goto-char (point-min))
                     (ansi-color-apply-on-region (point-min) (point-max))
                     (setq buffer-read-only t)
                     (view-mode)
                     (end-of-line)
                     ;; difftastic diffs are usually 2-column side-by-side,
                     ;; so ensure our window is wide enough.
                     (let ((width (current-column)))
                       (while (zerop (forward-line 1))
                         (end-of-line)
                         (setq width (max (current-column) width)))
                       ;; Add column size of fringes
                       (setq width (+ width
                                      (fringe-columns 'left)
                                      (fringe-columns 'right)))
                       (goto-char (point-min))
                       (pop-to-buffer
                        (current-buffer)
                        `(;; If the buffer is that wide that splitting the frame in
                          ;; two side-by-side windows would result in less than
                          ;; 80 columns left, ensure it's shown at the bottom.
                          ,(when (> 80 (- (frame-width) width))
                             #'display-buffer-at-bottom)
                          (window-width
                           . ,(min width (frame-width))))))))))))

The crucial parts of that helper function are that we "wash" the result using ansi-color-apply-on-region so that the difftastic highlighting using shell escape codes is transformed to emacs faces. Also, the needed width of the possibly wide side-by-side difftastic diff is computed and tried to be accommodated for.

Next, let's define our first command basically doing a git show for some revision which defaults to the commit or branch at point or queries the user if there's none.

(defun th/magit-show-with-difftastic (rev)
  "Show the result of \"git show REV\" with GIT_EXTERNAL_DIFF=difft."
  (interactive
   (list (or
          ;; If REV is given, just use it.
          (when (boundp 'rev) rev)
          ;; If not invoked with prefix arg, try to guess the REV from
          ;; point's position.
          (and (not current-prefix-arg)
               (or (magit-thing-at-point 'git-revision t)
                   (magit-branch-or-commit-at-point)))
          ;; Otherwise, query the user.
          (magit-read-branch-or-commit "Revision"))))
  (if (not rev)
      (error "No revision specified")
    (th/magit--with-difftastic
     (get-buffer-create (concat "*git show difftastic " rev "*"))
     (list "git" "--no-pager" "show" "--ext-diff" rev))))

And here the second command which basically does a git diff. It tries to guess what one wants to diff, e.g., when point is on the Staged changes section in a magit buffer, it will run git diff --cached to show a diff of all staged changes. If no context can be guessed, it'll query the user for a range or commit for diffing.

(defun th/magit-diff-with-difftastic (arg)
  "Show the result of \"git diff ARG\" with GIT_EXTERNAL_DIFF=difft."
  (interactive
   (list (or
          ;; If RANGE is given, just use it.
          (when (boundp 'range) range)
          ;; If prefix arg is given, query the user.
          (and current-prefix-arg
               (magit-diff-read-range-or-commit "Range"))
          ;; Otherwise, auto-guess based on position of point, e.g., based on
          ;; if we are in the Staged or Unstaged section.
          (pcase (magit-diff--dwim)
            ('unmerged (error "unmerged is not yet implemented"))
            ('unstaged nil)
            ('staged "--cached")
            (`(stash . ,value) (error "stash is not yet implemented"))
            (`(commit . ,value) (format "%s^..%s" value value))
            ((and range (pred stringp)) range)
            (_ (magit-diff-read-range-or-commit "Range/Commit"))))))
  (let ((name (concat "*git diff difftastic"
                      (if arg (concat " " arg) "")
                      "*")))
    (th/magit--with-difftastic
     (get-buffer-create name)
     `("git" "--no-pager" "diff" "--ext-diff" ,@(when arg (list arg))))))

What's left is integrating these two new commands in Magit. For that purpose, I've created a new transient prefix for my personal commands.

(transient-define-prefix th/magit-aux-commands ()
  "My personal auxiliary magit commands."
  ["Auxiliary commands"
   ("d" "Difftastic Diff (dwim)" th/magit-diff-with-difftastic)
   ("s" "Difftastic Show" th/magit-show-with-difftastic)])

I want my personal commands transient to be bound to # and be shown in the Magit dispatch transient (which is bound to ? in Magit status buffers and C-x M-g in any Magit enabled buffer) after the Run (!) transient.

(transient-append-suffix 'magit-dispatch "!"
  '("#" "My Magit Cmds" th/magit-aux-commands))

(define-key magit-status-mode-map (kbd "#") #'th/magit-aux-commands)

And that's it!

Finally, here's a screenshot showing how it looks like:

Screenshot of Magit showing a difftastic diff