diff --git a/ha-config.org b/ha-config.org index cd6df29..a32fd2e 100644 --- a/ha-config.org +++ b/ha-config.org @@ -1464,6 +1464,134 @@ I also need to append the following to my [[file:~/.gitconfig][~/.gitconfig]] fi plus-emph-style = syntax "#009000" plus-empty-line-marker-style = normal "#006800" #+end_src +*** Git with Difftastic +I’m stealing the code for this section from [[https://tsdh.org/posts/2022-08-01-difftastic-diffing-with-magit.html][this essay]] by Tassilo Horn, and in fact, I’m going to lift a lot of his explanation too, as I may need to remind myself how this works. The idea is based on using Wilfred’s excellent [[https://github.com/Wilfred/difftastic][difftastic]] tool to do a structural/syntax comparison of code changes in git. To begin, install the binary: +#+begin_src sh + brew install difftastic # and the equivalent on Linux +#+end_src +Next, we can do this, to use this as a diff tool for everything. +#+begin_src emacs-lisp + (setenv "GIT_EXTERNAL_DIFF" "difft") +#+end_src +But perhaps integrating it into Magit and selectively calling it (as it is slow). Tassilo suggests making the call to =difft= optional by first creating a helper function to set the =GIT_EXTERNAL_DIFF= to =difft=: +#+begin_src emacs-lisp :tangle no + (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)))))))))))) +#+end_src +The crucial parts of this helper function are that we "wash" the result using =ansi-color-apply-on-region= so that the function can transform the difftastic highlighting using shell escape codes to Emacs faces. Also, note the need to possibly change the width, as difftastic makes a side-by-side comparison. + +The functions below depend on [[help:magit-thing-at-point][magit-thing-at-point]], and this depends on the [[https://sr.ht/~pkal/compat/][compat]] library, so let’s grab that stuff: +#+begin_src emacs-lisp :tangle no + (use-package compat + :straight (:host github :repo "emacs-straight/compat")) + + (use-package magit-section + :commands magit-thing-at-point) +#+end_src +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. +#+begin_src emacs-lisp :tangle no + (defun th/magit-show-with-difftastic (rev) + "Show the result of \"git show REV\" with GIT_EXTERNAL_DIFF=difft." + (interactive + (list (or + ;; Use if given the REV variable: + (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)))) +#+end_src +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 it can not guess the context, it'll query the user for a range or commit for diffing. +#+begin_src emacs-lisp :tangle no + (defun th/magit-diff-with-difftastic (arg) + "Show the result of \"git diff ARG\" with GIT_EXTERNAL_DIFF=difft." + (interactive + (list (or + ;; Use 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)))))) +#+end_src + +What's left is integrating the new show and diff commands in Magit. For that purpose, Tasillo created a new transient prefix for all personal commands. Intriguing, but I have a hack that I can use on a leader: +#+begin_src emacs-lisp :tangle no + (defun ha-difftastic-here () + (interactive) + (call-interactively + (if (eq major-mode 'magit-log-mode) + 'th/magit-show-with-difftastic + 'th/magit-diff-with-difftastic))) + + (ha-leader "g d" '("difftastic" . ha-difftastic-here)) #+end_src *** Time Machine The [[https://github.com/emacsmirror/git-timemachine][git-timemachine]] project visually shows how a code file changes with each iteration: