【技巧分享+问题已解决】 在 magit 中使用 difftastic

技巧分享

difftastic 是 Emacs 插件 deadgrep 作者写的一个根据语法进行结构性对比的命令行工具。

brew install difftastic
git config --global diff.external difft

然后我在 highlight-parentheses.el 作者的博客中学(抄)到了怎么在 magit 中使用 difftastic

https://tsdh.org/posts/2022-08-01-difftastic-diffing-with-magit.html

(require 'magit)
(defun my/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))))))))))))
(defun my/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")
    (my/magit--with-difftastic
     (get-buffer-create (concat "*git show difftastic " rev "*"))
     (list "git" "--no-pager" "show" "--ext-diff" rev))))
(defun my/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) "")
                      "*")))
    (my/magit--with-difftastic
     (get-buffer-create name)
     `("git" "--no-pager" "diff" "--ext-diff" ,@(when arg (list arg))))))
(transient-define-prefix my/magit-aux-commands ()
  "My personal auxiliary magit commands."
  ["Auxiliary commands"
   ("d" "Difftastic Diff (dwim)" my/magit-diff-with-difftastic)
   ("s" "Difftastic Show" my/magit-show-with-difftastic)])
(transient-append-suffix 'magit-dispatch "!"
  '("#" "My Magit Cmds" my/magit-aux-commands))

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

效果非常好

求助

只是有一个问题不太明白,如果我不写 (require 'magit) 运行会报错,但是写了又会让 magit 占用 Emacs 启动时间,有没有办法让这段代码在 magit 之后生效的方法啊?

试了下用 (with-eval-after-load 'magit )把上述代码包住,Emacs 启动时不报错了,但是还会在运行中报各种错误。

跟 这个 有区别吗? 都是 rust 写的

delta 作者居然有 magit-delta 插件!我切换过去试试。

感谢推荐。

不幸的是 delta 恰好不支持 Emacs Lisp。

我不是程序员,现在 Emacs Lisp 对我是最重要的……

delta我用过,太花哨了,看diff很干扰的。

difftastic其实没有那么完美,支持语言不多。

还是默认diff比较好,够用,所有语言都支持。

作者的团队需要一款基于语法的 merge 工具才催生了 difftastic。

difftastic 背靠 tree-sitter,发展前景应该是不错的,新出的 0.4 说是性能改善了不少。

我不是程序员,所以支持 Emacs Lisp 就是对我最重要的事了 :laughing:

不要对语法 merge 这个概念有过高的期望, 因为有时候补丁并不是按照语法去变化的, diff 的关键是要清晰, 同时要方便提取片段, 过于强调语法 diff, 其实有时候不方便撤销补丁。

同理, delta 那种颜色比默认 diff 丰富的多, 但是长期用下来, 其实反而很干扰。

有些事情, 简单的更好, 复杂的反而不好用。

2 个赞

:grimacing: 其实我只是个颜控,这几个工具的功能对我来说都是过剩的。

delta 和 difftastic 我都用过, 主要是开发 eaf-git-client 的时候测试过, 一开始都是奔着功能更强大去的, 其实不好用。

delta 仓库里没有 tree-sitter 关键词,应该是用了自己的一套 diff 算法吧。不过不支持 Emacs Lisp 这一点就足够在我的名单里 pass 掉了。

difftastic 是真的帅,它知道我的 elisp 只动了一行代码。它懂我,功能和性能差点我也能忍。 :laughing:

(with-eval-after-load 'magit
  ;; your code
  )

看来这是唯一的方法了,可能报错的问题根源不在我的设置里。

运行中报什么错?

如果你需要在magit尚未载入的情况下调用你的命令,你可以在你的命令开头加入(unless (featurep 'magit) (require 'magit))

:rofl: 刚又触发了一下,不是报错,是 warning。

刚刚超过了 undo-outer-limit 默认最大值 447636 byte :laughing:

⛔ Warning (undo): Buffer ‘*git diff difftastic 4b9caba^..4b9caba*’ undo info was 24313651 bytes long.
The undo info was discarded because it exceeded `undo-outer-limit'.

This is normal if you executed a command that made a huge change
to the buffer.  In that case, to prevent similar problems in the
future, set `undo-outer-limit' to a value that is large enough to
cover the maximum size of normal changes you expect a single
command to make, but not so large that it might exceed the
maximum memory allotted to Emacs.

If you did not execute any such command, the situation is
probably due to a bug and you should report it.

You can disable the popping up of this buffer by adding the entry
(undo discard-info) to the user option `warning-suppress-types',
which is defined in the `warnings' library.

⛔ Warning (undo): Buffer ‘*git diff difftastic 4b9caba^..4b9caba*’ undo info was 24447636 bytes long.
The undo info was discarded because it exceeded `undo-outer-limit'.

This is normal if you executed a command that made a huge change
to the buffer.  In that case, to prevent similar problems in the
future, set `undo-outer-limit' to a value that is large enough to
cover the maximum size of normal changes you expect a single
command to make, but not so large that it might exceed the
maximum memory allotted to Emacs.

If you did not execute any such command, the situation is
probably due to a bug and you should report it.

You can disable the popping up of this buffer by adding the entry
(undo discard-info) to the user option `warning-suppress-types',
which is defined in the `warnings' library.

没想到 difftastic 能产生这么多数据……