Topics

Diff

magit#Diffing

magit#Ediffing

magit 如何管理 conflicts?

Resolving Conflicts (Magit User Manual)

Git commands run through magit-start-process wire Magit’s sentinel/filter to every process, so a merge that exits with conflicts

leaves the Git output in the process buffer and triggers a status refresh (magit/lisp/magit-process.el:662).

  • Detection is driven by magit-anything-unmerged-p and magit-unmerged-files, both wrappers around git ls-files —unmerged; anything

    non-empty marks the repo “conflicted” (magit/lisp/magit-git.el:1115-1330). Those checks gate operations like magit-commit, which

    refuses to run while conflicts remain (magit/lisp/magit-commit.el:457), and they feed prompts such as magit-read-unmerged-file

    (magit/lisp/magit-files.el:583).
  • The status buffer gets rebuilt with the unresolved files folded into the diff sections. magit-insert-unstaged-changes re-runs

    git diff so conflict markers appear inline, and dedicated rendering code highlights <<<<<<< /=======/>>>>>>> (magit/lisp/magit-

    diff.el:1333, magit/lisp/magit-diff.el:3154, magit/lisp/magit-diff.el:2258).
  • For resolving a single hunk Magit calls into smerge: commands like magit-smerge-keep-upper invoke the appropriate smerge-* helper

    on the diff hunk at point and restage the file (magit/lisp/magit-apply.el:780-828).
  • For full-file merge resolution magit-ediff-resolve-all/magit-ediff-resolve-rest open an Ediff session with ours/theirs/ancestor

    buffers, save the result, and restage once the file is clean (magit/lisp/magit-ediff.el:214-315).
  • When you prefer the raw Git stages, magit-checkout-stage lets you check out —ours, —theirs, or restore the conflict in one step

    (magit/lisp/magit-merge.el:241); the command menu wires these options into the status buffer.
  • After conflicts are cleared, the next refresh sees magit-anything-unmerged-p return nil, so guarded operations (commit, rebase-

    continue, etc.) are unblocked and Magit stops flagging the repository as conflicted.

This combination of process monitoring, conflict detection, UI highlighting, and resolution helpers is what allows Magit to “just

work” on Windows or Linux for merging and rebasing conflicts.

About magit-process

magit-process.el Responsibilities

  • Defines the core Git process runner (magit-start-process) which wraps start-file-process, attaches Magit’s sentinel/filter, and

    records metadata for later refreshes (magit/lisp/magit-process.el:662).
  • Provides convenience entry points such as magit-run-git-async and magit-run-git-with-editor to launch Git commands (the latter

    first exports $GIT_EDITOR) (magit/lisp/magit-process.el:597, magit/lisp/magit-process.el:605).
  • Manages the process environment (magit-process-environment) so Windows-specific tweaks like CYGWIN=noglob or code-page handling

    are applied only when needed (magit/lisp/magit-process.el:520).
  • Maintains the process buffer UI: filters append Git output, sentinels display status and trigger buffer refreshes after the

    command exits (magit/lisp/magit-process.el:714, magit/lisp/magit-process.el:742).
  • Records commands for debugging, enforces UTF-8 I/O on Windows, and interacts with Magit sections so each running Git process

    appears in the status/process buffers with live state (magit/lisp/magit-process.el:528, magit/lisp/magit-process.el:700).

completion 管理

Completion and Confirmation

magit#Completion Confirmation and the Selection

我需要更加优秀的 completion 控制

magit-completing-read

magit-dwim-selection

Transient

他已经是 emacs Built-In 的包了!!!

  • set defaults(transient-set-and-exit ):

    把当前 transient 的值“设置”为本次 Emacs 会话内的默认值(写到内存里的 transient prototype/history),

    下次再打开同一个 transient 会用这个值,但重启 Emacs 会丢。
  • save defaults(transient-save-and-exit ):

    把当前值持久化保存到 transient-values(并写入 transient 的保存文件,通常是 ~/.emacs.d/transient/values.el 或等价路径),

    我是 </home/WD/.config/emacs/.local/etc/transient/values>

    重启 Emacs 也还在。

经验:transient-get-value vs transient-args

  • transient-get-value :读取“当前正在显示的 transient(extant prefix)”的实时值;适合在菜单渲染阶段(例如 :description / =:suffix-description=)使用。
  • transient-args :更偏向给 suffix 命令用;当 transient-current-command 不是该 prefix 时会回退到 set/saved/default 值,所以在菜单尚未 export 的场景里容易读到旧值。
  • 大多数 infix 默认走 transient—do-stay ,不会 export transient-current-command

    因此在 transient 菜单仍打开时,想“看见刚刚改过的参数是否生效/是否高亮”,应该读 transient-get-value ,而不是 transient-args
  • 如果用普通 suffix 命令(不是 infix 本身)去改某个 infix 的 value(例如从 section toggle =—from/—to=),改完后要调用 transient—redisplay (或等价刷新)才能让菜单立即更新“亮起/值显示”。
  • :incompatible + transient-infix-set:after 逻辑会自动清理互斥参数;自定义 infix 类/方法时要先 (cl-call-next-method),否则互斥清理会失效。
  • 如果你把 transient 的值抽成列表再手动 append,很容易出现 --summary 这种重复;

    更像 Magit 的做法是让 infix 对象持有唯一 value,再由 transient 汇总得到参数列表。

Issues

suffix 命令在用 edebug 调试时,不能正确得到参数!

应该先看一遍文档

transient#Top

中缀后缀是什么?

Emacs 的 transient 模型借用了“前缀 + 选项 + 动作”的结构,因此区分了两类子命令:

  • Infix 命令承担“设置选项/参数”的角色:在 transient 菜单仍然打开时,让用户快速调整某个变量或开关,菜单会立即显示新的参数值,但不会离开当前状态。
  • Suffix 命令是“执行动作/结束”的部分:它通常读取已经由 infix 设置好的参数,完成真正的操作,然后退出 transient。

这种区分把“配置”和“执行”分离开来,让你在同一套快捷键里先连续设置多个参数,再一次性触发最终命令,同时还能让某些 infix 拥有副作用(直接修改变量)而不必立即执行主命令。

transient-non-suffix

是 prefix 的 属性是在说这个 prefix transient 能不能运行除了 suffix 意外的命令,这在 majutsu 中是广泛应用的。

abort and resume

C-g

先终止当前未完成的按键序列;

f若没有,则仅退出当前活跃的 transient 并回到上一级(如果有嵌套)。

适合在同一 transient 层级里撤销一次错误的键或返回上一菜单。

transient-quit-all C-q

终止未完成的按键序列后,直接退出所有正在运行或挂起的 transients,等于彻底离开整个 transient 堆栈。

transient-suspend C-z

行为像 C-q,但会把当前的 transient 堆栈保存下来,方便之后用 M-x transient-resume 回到之前的状态(包含嵌套层级与 infix 配置)。

transient-resume

在使用 C-z 暂停后,恢复那时保存的那一整套 transient 堆栈;如果之前没有保存则无效果。

3 Modifying Existing Transients

transient#Modifying Existing Transients

transient-append-suffix

(transient-append-suffix PREFIX LOC SUFFIX &optional KEEP-OTHER)

Insert a SUFFIX into PREFIX after LOC.

5 Classes and Methods

transient#Classes and Methods

Transient 通过类与泛型函数定义新的前缀/后缀类型;

这些类型可以与现有类型外观相近,但在行为上有所差异。

每个 prefix、infix、suffix 命令都关联到一个对象,用来存储控制其行为的信息。关联方式有两种:

  • 给命令指定类,以便根据类型调用不同的泛型函数(用于“同类共用行为”)。
  • 给命令关联对象,用来存放该命令独有的数据(比如按键绑定、描述等;用于“同类之间也可能不同的行为”)。

某些槽位(例如 =reader=)保存普通函数,用来读取 infix 的新值;

泛型函数负责同类共享的行为,槽位中的函数负责实例级(或同类内部差异)的行为。

Group Classes

:class 设定组类型,例如 =[:class transient-columns …]=(见 transient-columns)。

  • transient-childtransient-grouptransient-suffix 的抽象基类,使组内可以包含其他组(见 transient-child)。
  • transient-group 是所有组类的父类(见 transient-group)。
  • =transient-column=:默认的“扁平”组,一行一个元素(见 transient-column)。
  • =transient-row=:所有元素显示在同一行(见 transient-row)。
  • =transient-columns=:元素按列排布。每个直接元素必须是组,组内再放命令或字符串。默认嵌套组类型(首元素若为向量即使用它)(见 transient-columns)。
  • =transient-subgroups=:包裹多个组,组之间插入空行;子组自行决定内部显示(见 transient-subgroups)。

Group Methods

Prefix Classes

内置仅有 =transient-prefix=(见 transient-prefix),但包作者可以自定义子类,通过实现专用方法改变同类前缀的行为。

前缀命令的对象保存在命令符号的 transient--prefix 属性里;激活 transient 时会克隆该对象并存入 transient—prefix,这样在当前会话中的临时改动不会影响下次调用。

定义前缀命令使用:

(transient-define-prefix NAME ARGLIST [DOCSTRING] [KEYWORD VALUE]... GROUP... [BODY...])

Suffix Classes

所有 suffix/infix 类都派生自 =transient-suffix=,它又源自 =transient-child=(见 transient-suffix 与 transient-child)。

所有 infix 类派生自抽象的 =transient-infix=(见 transient-infix)。它们默认使用 transient—do-stay 作为 =pre-command=,以确保执行后仍停留在 transient。若自创 infix 类不继承它,往往需要自行实现更多方法。

表示命令行参数的 infix 类应继承 =transient-argument=(见 transient-argument)。

常见子类包括:

  • =transient-switch=:无值的开关型参数(见 transient-switch)。
  • =transient-option=:需要取值的选项(见 transient-option)。
  • =transient-switches=:互斥开关集合(见 transient-switches)。
  • transient-files=:处理 =-- 之后“全部当作文件”的参数(见 transient-files)。

表示变量的 infix 类继承 =transient-variable=(见 transient-variable)。

  • transient-information / transient-information*=:只展示信息的“假 suffix”,无命令也无按键,可用 =(:info ...) / (:info* ...) 定义;带 * 的版本会把描述对齐到按键列(见 transient-information 与 transient-information*)。
  • =transient-lisp-variable=:显示/修改 Lisp 变量(尚未完全稳定)(见 transient-lisp-variable)。
  • transient-cons-option=:让 fn:transient-args 返回 alist,用 =(:cons OPTION :key KEY ...) 定义(实验性)(见 transient-cons-option)。
  • 其他特殊类如 transient-describe-target=、=transient-value-preset 等,以及 Magit 自带的扩展类。

定义 infix 与 suffix 命令分别使用:

Prefix Methods

Suffix Methods

值相关:

显示相关:

Prefix Slots

值/作用域相关槽位:

  • default-value=、=init-value=、=history-key=、=incompatible=(互斥参数列表)、=scope

行为相关槽位:

  • transient-suffix / transient-non-suffix / =transient-switch-frame=(控制执行后是否保持 transient)
  • =refresh-suffixes=(每次命令后刷新;代价较高)
  • =environment=(初始化菜单时的共享环境)

外观相关槽位:

  • display-action=、=mode-line-format=、=column-widths=、=variable-pitch

文档相关槽位:

  • show-help=、=man-page=、=info-manual=、=suffix-description

内部槽位(实现细节):

  • prototype=、=command=、=level=、=value=、=history=、=history-pos=、=unwind-suffix

Suffix Slots

transient-child 公共槽位:

  • parent=,以及各种 =if*/inapt-if* 条件与 =level=(见下文 “Predicate Slots”)

transient-suffix 槽位:

  • =key=、=command=、=transient=(执行后是否保留菜单)
  • format=、=description=、=face
  • show-help=、=summary
  • definition
  • advice / =advice*=(为命令添加环绕/interactive advice)

transient-infix 槽位:

  • argument=、=shortarg
  • value=、=init-value=、=unsavable
  • multi-value=(=rest / =repeat=)
  • always-read=、=allow-empty
  • history-key
  • reader=、=prompt=、=choices

其他特定子类槽位:

  • transient-variablevariable
  • transient-switchesargument-format / argument-regexp

Predicate Slots

控制“显示/隐藏”:

  • if=、=if-not=、=if-non-nil=、=if-nil
  • if-mode=、=if-not-mode
  • if-derived=、=if-not-derived

控制“可执行/灰显”(inapt):

  • inapt-if=、=inapt-if-not=、=inapt-if-non-nil=、=inapt-if-nil
  • inapt-if-mode=、=inapt-if-not-mode
  • inapt-if-derived=、=inapt-if-not-derived

这些谓词默认在前缀被调用时计算;若前缀设置了 =refresh-suffixes=,则每次执行后会重新计算。

level(1–7)用来按用户偏好隐藏低优先级命令,详见“Enabling and Disabling Suffixes”。

with-editor

我们需要尝试使用这个包!

它是基于 emacsclient 实现的

完全没问题:在 Majutsu 里调用 with-editor 并不要求你提前跑 emacs —daemon 或 emacsclient。with-editor—setup 会在当

前 Emacs 实例里确保 server 已经启动(必要时自动 server-start),然后让 jj 通过 emacsclient 回连到“当前这台 Emacs”。

所以只要你是在 Majutsu/Emacs 里执行 commit/describe,就算没有任何守护进程,也能照常弹出缓冲区、C-c C-c 完成提交。

with-editor

with-editor*

我想要知道 with-editor 是如何管理新打开的窗口的

with-editor-server-window-alist

server-window

git-commit-filename-regexp

这个文件名也是有规则的

/tmp/editor-xxxxxx.jjdescription

with-editor-filter-visit-hook

Magit 并不是单纯依赖 with-editor 的默认行为,而是在加载 with-editor 之后做了两件事来掌控编辑缓冲区出现的位置:

  1. 注册 with-editor-server-window-alist

    在 lisp/magit-commit.el 里你会看到这段代码:

       (add-to-list 'with-editor-server-window-alist
                   (cons git-commit-filename-regexp #'switch-to-buffer))

    with-editor 在调用 server-switch-buffer 时,会查这个 alist——只要 buffer 的文件名匹配某个正则,就把配套的函数当

    成“窗口选择器”来用。Magit把所有 COMMIT_EDITMSG 之类的文件映射到 switch-to-buffer,也就是“就在当前窗口显示”,从而

    避免被 display-buffer 的默认策略带到别的 window/frame 去。同理,git-rebase.el 里也会往这个 alist 里塞自己的规则,

    保持 rebase todo buffer 的位置。

  2. 配合 server-switch-hook / with-editor-filter-visit-hook

    例如 magit-commit-diff 会被挂到这两个 hook 上:每当 commit message buffer 打开时,Magit会自动弹一个 diff buffer、

    调整布局等。这一步负责“连带”地布置其它视图,而前一步负责主编辑缓冲区到底在哪个窗口显示。

总结一下:Magit靠向 with-editor-server-window-alist 塞定制条目来决定“with-editor 缓冲区放在哪个 window”,并利用

server-switch-hook/with-editor-filter-visit-hook 做额外布局动作。你如果要复制这种行为,就可以把自己的模式/文件名正则

和想用的窗口选择函数加到 with-editor-server-window-alist 里,这样 with-editor 打开的 buffer 就会按照你的规则出现。

magit-section

他是独立发布的!!!

独立发布

  • 2020 年 1 月,Magit 作者 Jonas Bernoulli 将 magit-section.el 从 Magit 主包中“拆分”出来。尽管源文件仍保存在同一个 Git 仓库中,但它拥有独立的包描述、手册与版本号。这样,Elisp 生态中的其他项目就能像依赖普通库一样依赖 =magit-section=,而无需安装整个 Magit。(emacsair.me)
  • 拆分后,=magit-section= 在 NonGNU ELPA 上以单独 tarball 形式发布(当前稳定版 4.4.2 于 2025-10-06 发布)。页面明确说明“曾经是 Magit 的一部分,但现在可供无关项目使用”。该独立发布流水线与 Magit 本体并行运行:版本号大体保持同步,但出包时间可单独决定。(elpa.nongnu.org)
  • 发布公告同样独立:NonGNU ELPA 的机器人会分别推送 magit-section 与 Magit 的更新邮件(例如 2025-07-05 公告 4.3.8 版),表明在 ELPA 基础设施中它们就是两个互不绑定的包。(lists.gnu.org)
  • 独立的开发者手册 *Magit-Section Developer Manual*(目前对应 4.4.2 版)单独维护,讲解如何在第三方包中复用 Magit 风格的可折叠 section UI,进一步强化其“可复用组件”的定位。(docs.magit.vc)
  • 发行版也据此打包:Debian 提供 elpa-magit-section 二进制包(源码仍来自 magit 仓库,但打包与依赖元数据独立),说明在更广泛的发布渠道中它已作为独立产品进行管理。(packages.debian.org)

通过在同一源码库中维护多套 Package-Requires / 手册,借助 NonGNU ELPA 的多配方打包机制,并让发行版分别打包,=magit-section= 虽与 Magit 共用代码与仓库,但在发布、依赖与文档层面已作为独立包运作。

最重要的类型:magit-section

(defclass magit-section ()
  ((type     :initform nil :initarg :type)
   (keymap   :initform nil)
   (value    :initform nil)
   (start    :initform nil)
   (content  :initform nil)
   (end      :initform nil)
   (hidden)
   (painted)
   (washer   :initform nil :initarg :washer)
   (inserter :initform (symbol-value 'magit--current-section-hook))
   (selective-highlight    :initform nil :initarg :selective-highlight)
   (heading-highlight-face :initform nil :initarg :heading-highlight-face)
   (heading-selection-face :initform nil :initarg :heading-selection-face)
   (parent   :initform nil)
   (children :initform nil)))

:type

这里的 type 的作用是作为一种临时 section 类型

可以在 magit-get-section 和 magit-insert-section 之间搭配使用

主要的发现是 diffstat 中使用了这种临时 section 实现,下面是找到的参考

</.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

<
/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

washer 是干什么用的?

washer 就是“延迟渲染”用的 thunk(函数闭包),在 section 被第一次展开时才执行,用来把正文真正插进去。

工作链条(源码都在 magit/lisp/magit-section.el):

  • magit-section 类的 washer slot 定义在 396–411 行,默认为 nil。
  • 用 magit-insert-section-body 包裹正文时,如果这个 section 初始是隐藏的(hidden 为 t),宏不会立即执行 BODY,而是把一个

    lambda 存进 washer(1553–1576 行)。那个 lambda 里会临时调整 marker insertion-type,执行 BODY,补上 text properties 等。
  • 展开时 magit-section-show 会调用 magit-section—opportunistic-wash(913–918、1667–1676 行):发现 washer 非空就执行它,把

    正文插入、更新 content/end,然后把 washer 置回 nil。
  • magit-section-content-p 把“有 washer 但还没洗过”的 section 也算作有内容(1064–1067 行),因此后续逻辑(键位、可见性指示器

    等)仍生效。

核心作用:提高插入性能、避免在默认折叠的 section 上浪费时间,同时保证第一次展开时再“洗”(wash)出真正的内容。

value

是用来存真正有用的数据的

value 的通用语义

  • 在 Magit/majutsu 的 section 系统里,value 就是这个 section 的“payload/上下文数据”,

    由 (magit-insert-section (TYPE VALUE …) …) 写进去(或之后再 oset)。

    magit/lisp/magit-section.el:396

    magit/lisp/magit-section.el:1339

    magit/lisp/magit-section.el:1409

它主要在哪些地方起作用

  • 刷新后定位“同一个 section”(用于继承折叠/展开等状态):

    magit-section-ident 用 type + (magit-section-ident-value section) 组成标识。

    magit/lisp/magit-section.el:545

    magit/lisp/magit-section.el:558

  • 交互命令的上下文:很多命令用 magit-section-value-if/magit-region-values 直接把 value 当作“point/region 代表的对象”。

    magit/lisp/magit-section.el:1289

    magit/lisp/magit-section.el:2153

常见 section 的 value 放什么(源码例子)

  • commit→hash/rev;

    file→文件路径;

    hunk→(about . ranges);

    branch/tag/remote→名字;

    unpulled/unpushed→rev-range;

    process→process 对象。

    magit/lisp/magit-log.el:1414

    magit/lisp/magit-diff.el:2527

    magit/lisp/magit-diff.el:2659

    magit/lisp/magit-status.el:589

    magit/lisp/magit-status.el:689

    magit/lisp/magit-status.el:731

    magit/lisp/magit-log.el:1943

    magit/lisp/magit-process.el:728

chat

下面按 magit/lisp/magit-section.el 定义逐个说明各 slot 作用与在 Magit 中的典型使用场景(括号标出参考行号,便于对照源码;行

号以当前仓库版本为准)。

  • type
    • 含义:节的“种类”符号。
    • 用途:匹配/分支逻辑(magit-section-match、magit-section-case)、导航跳转(magit-get-section 逐层比对 type)、显示决定(如可见性规则)。
  • keymap
    • 含义:节专属按键映射(可为符号或 keymap)。
    • 用途:在标题行与通用 magit-section-heading-map 组合,提供节特定快捷键(magit-section-maybe-add-heading-map 1617行)。
  • value
    • 含义:节携带的数据(文件名、提交对象等)。
    • 用途:构造唯一标识 magit-section-ident(545行起);命令依据 value 处理当前节(如 diff/hunk 操作、imenu 生成等)。
  • start / content / end
    • 含义:节在缓冲区的起点、标题结束/正文开始位置、正文结束位置。
    • 用途:折叠/展开定位与隐藏(magit-section-show/hide 913/928行)、绘制可视化指示器、按键映射作用范围、光标跳转(magit-section-goto-successor 等)。
  • hidden
    • 含义:正文是否折叠。
    • 用途:控制显示;展开时触发 washer、重绘可见性符号(magit-section-show 913行)。
  • painted
    • 含义:当前高亮状态标记。
    • 用途:避免重复着色,配合 magit-section—opportunistic-paint 在聚焦/失焦时更新高亮(1880行起)。
  • washer
    • 含义:延迟渲染函数(thunk)。
    • 用途:当节初始被折叠且正文通过 magit-insert-section-body 注册,展开时 magit-section—opportunistic-wash(1667行)调用它插入正文,提高初次渲染性能;magit-section-content-p(1065行)把“尚未洗但有 washer”视为有内容,以便快捷键/指示器正常工作。
  • inserter
    • 含义:记录创建该节的插入器函数(默认当前 section hook)。
    • 用途:magit-describe-section(约1160行)追溯节来源,调试/帮助时显示“由哪个 inserter 生成”。
  • selective-highlight
    • 含义:强制该节可被高亮的标志。
    • 用途:在 selective 高亮策略下,覆盖默认行为,保证当前节或其子节能被着色(见 1879 行高亮逻辑)。
  • heading-highlight-face / heading-selection-face(408–409行)
    • 含义:标题在聚焦/选中时使用的 face。
    • 用途:替换默认标题高亮/选中外观(同样在高亮流程 1879 行起被应用)。
  • parent / children
    • 含义:树形结构的父节与子节列表。
    • 用途:几乎所有结构操作:
      • 路径匹配与跳转:magit-section-ident/magit-get-section 依靠 parent→children 链定位。
      • 展开/折叠传播:magit-section-show/hide 递归对子节操作。
      • 导航:magit-section-forward/backward/siblings 使用 children 计算移动。
      • 可见性缓存、计数、嵌套高亮等都基于这棵树。

概括:

这些 slot 共同支撑 Magit 的“折叠式 section 树”——type/value 给出身份,

start/content/end/hidden/washer 管显示与延迟渲染,keymap/face 管交互与样式,parent/children 管结构,

inserter/selective-highlight/painted 管元数据与高亮。

magit-insert-section 详细分析

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-section.el>

magit-insert-section 宏在干什么

  • 宏定义于 lisp/magit-section.el:1339。

    它在当前位置创建并插入一个Magit section 对象,执行用户提供的 BODY 来写出标题和内容,然后返回新建的 section 对象。
  • 宏本身只负责“框架”:解析参数、创建对象、设置父子关系、运行 BODY、收尾(标记范围、属性、加入父节点子表)。

    真正的插入工作由 magit-insert-section—create 与 magit-insert-section—finish 完成(同文件约 1408、1448 行)。

declare 解释

关于 declare 的更多信息查看 Declare Form

(declare (indent 1) ;sic
        (debug ([&optional symbolp]
                (&or [("eval" form) &optional form form &rest form]
                    [symbolp &optional form form &rest form])
                body)))

但是

(magit-insert-section _hunk-section (majutsu-hunk-section file nil :file file :header header)
                      (magit-insert-heading
                        (propertize header 'font-lock-face 'magit-diff-hunk-heading))
                      (dolist (line lines)
                        (insert (propertize line
                                            'font-lock-face
                                            (cond
                                             ((string-prefix-p "+" line) 'magit-diff-added)
                                             ((string-prefix-p "-" line) 'magit-diff-removed)
                                             (t 'magit-diff-context))))
                        (insert "\n"))))

没有符合你的形式啊

─────────────────────────────────────────────────────────────────────────────────────────────

你的调用其实正好符合 declare 里的 Edebug 模板,逐项对照一下就明白了:

  • 形参展开是 [NAME] (CLASS &optional VALUE HIDE) &rest BODY。
    • NAME → _hunk-section(可选符号,匹配 symbolp)。
    • 第二个实参是一个列表 (majutsu-hunk-section file nil :file file :header header),需要

匹配 (&or [(“eval” form) &optional form form &rest form] [symbolp &optional form form &rest

form]) 里的第二个分支:

  • symbolp:majutsu-hunk-section ✔
  • 第 1 个 &optional form:file(作为 VALUE)
  • 第 2 个 &optional form:nil(作为 HIDE)
  • &rest form:(:file file :header header)(余下的构造参数)
  • BODY 就是后面的 (magit-insert-heading …) (dolist …),匹配 body。

所以这条调用完全符合 declare 里的 debug 规范;可能看上去“不像”,只是因为模板是用列表模式描述的,

Edebug 用方括号 [ … ] 表示“这是一个列表参数”,而你已经正好给了一个以符号开头的列表参数。

所以你到底怎么解释

(magit-insert-section
    ( message nil nil
              :heading-highlight-face 'magit-diff-revision-summary-highlight)
  (let ((beg (point)))
    (forward-line)
    (magit--add-face-text-property
     beg (point) 'magit-diff-revision-summary))
  (magit-insert-heading)
  (if (re-search-forward "-----BEGIN PGP SIGNATURE-----" nil t)
      (goto-char (match-beginning 0))
    (goto-char (point-max)))
  (insert ?\n))

这种只有一个列表参数的情况?

这个例子也完全匹配 declare 的 debug 模板,只是省掉了可选的 NAME:

  • 模板第一段 [&optional symbolp]:可选的 NAME,这里没给,直接跳过。
  • 第二段 (&or [(“eval” form) &optional form form &rest form] [symbolp &optional form form

    &rest form]):必须给一个列表实参。
    • 你的列表是 (message nil nil :heading-highlight-face ‘magit-diff-revision-summary-

      highlight),走的是第二个分支(以符号开头)。
      • symbolp:message 作为 CLASS。
      • 第 1 个 &optional form:nil,充当 VALUE。
      • 第 2 个 &optional form:nil,充当 HIDE。
      • &rest form:(:heading-highlight-face ‘magit-diff-revision-summary-highlight),额外 init 参数。
  • 后面的所有 forms 都是 body,对应模板里的 body 部分(let、magit-insert-heading、搜索签名、insert 等)。

因此“只有一个列表参数”正是宏预期的第二形态:省略 NAME,直接给 (CLASS VALUE HIDE . ARGS),后面

跟 BODY。 indentation 仍按 (indent 1) 规则:第一个实参(这里就是这个列表)视为“头”,其余视为主体。

参数解析与调用形式

  • 形如 (magit-insert-section [NAME] (CLASS &optional VALUE HIDE) &rest

    BODY);NAME 若给出,会在 BODY 内绑定到本次 section 对象。
  • CLASS 可以是类符号,或 (eval FORM) 运行时求值;VALUE 会写入对象的

    value 槽;HIDE 仅用于首次创建时默认折叠。
  • 其它 args(CLASS 列表中多出的参数)直接传给 apply class :type type

    args,用来初始化额外槽位。

展开后的执行流程

  • 解析可选的 NAME 绑定,抽取 (class value hide . args)。
  • 调用 magit-insert-section—create 生成对象:确定类型符号,填充 value、

    parent、start,计算默认折叠状态,并推断keymap(magit-<type>-section-

    map 或 forge-<type>-section-map)。
  • 动态绑定 magit-insert-section—current、—parent 指向新对象;若当前

    无父节点,则把 magit-root-section 改为新对象并记录旧根到—oldroot,

    用于刷新时继承可见性。
  • 执行 BODY(可用 magit-cancel-section 在内部 (throw ‘cancel- section

    …) 终止并清理)。
  • 调用 magit-insert-section—finish 完成本次插入。

创建阶段的细节 (magit-insert-section—create, 1408+)

  • 类型解析:若 class-p,尝试在 magit—section-type-alist 找到对应的

    “type 符号”;否则把 CLASS 当作 type,再从该 alist 找出真正的类,

    找不到则回退到 magit-section 基类。
  • 可见性:按顺序检查 magit-section-set-visibility-hook、旧根中相

    同 ident 的历史可见性(刷新场景且未禁用继承)、magit-section-

    initial-visibility-alist,最后才看传入的 HIDE。
  • 起始位置:若 magit-section-inhibit-markers 为真,用数值位置;否则

    用 marker。
  • keymap:若对象还没有 keymap 槽,尝试绑定到 magit-<type>-section-

    map 或 forge-<type>-section-map 符号(若存在)。

收尾阶段的细节 (magit-insert-section—finish, 1448+)

  • 先运行 magit-insert-section-hook,然后设置 end(数字或 marker);

    对于延迟 marker 模式(magit-section-inhibit-markers 为 delay 且当

    前为根),完成后把所有 start/end 转成复制的 marker。
  • 为非根 section 设置文本属性、标题 keymap,若有子节点插入 child-

    count。
  • 把当前对象加入父节点的 children 列表;若 magit-section-insert-in-

    reverse 为真则先逆序累积再整体反转,便于日志类反向插入。
  • 返回插入的对象;若在 BODY 中调用 magit-cancel-section 则不会到达

    此处,也不会残留内容。

使用/调试提示

  • 在 BODY 中写标题和内容必须前移 point,否则 start/end 会重合。
  • 需要条件性创建时,用 magit-cancel-section 让半成品彻底撤销。
  • 刷新场景下(典型 Magit buffer 更新),HIDE 只作用首次创建;可见性

    主要继承旧 section 状态,加上自定义 hook/visibility alist。

关于 section 的比较:magit-section-match

</.config/emacs/.local/straight/repos/magit/lisp/magit-section.el>

<
/.config/emacs/.local/straight/repos/magit/lisp/magit-section.el>

magit-section-match

就是 -1 的 wrapper

magit-section-match-1

处理的是 (CONDITION…) matches if any of the CONDITIONs matches.

magit-section-match-2

处理

[CLASS…] matches if the section’s class is the same

as the first CLASS or a subclass of that;

the section’s parent class matches the

second CLASS; and so on.

[* CLASS…] matches sections that match [CLASS…] and

also recursively all their child sections.

magit-section-case 基本上就是用 match 实现的 magit-section 版的 pcase

Magit Dev Notes

Inbox

这里包装了 get-buffer-createget-buffer

magit-get-revision-buffer

magit-get-revision-buffer-create

有一个 revision-buffer 的概念

这里用到了 get-buffer-create

elisp#Creating Buffers

另外一种创建 buffer 的方式是 generate-new-buffer

get-buffer elisp#Buffer Names

这是在干什么???怎么还连了一个 doomemacs 的 issue

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-files.el>

magit-blob-mode

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-files.el>

magit-blob-visit

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-files.el>

magit-diff-visit-file

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

我之前以为这是什么核心的函数,但是其实它的作用是跳到 {worktree} 的最新的当前文件

我想要知道 diff 是如何处理在恰当的时候折叠的

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

我需要学习处理一下 magit-seciton 的 visibility 管理

magit-section-set-visibility-hook

porcelain

porcelain

在 Git 语境里 “porcelain” 指的是面向用户的高层命令 / 界面(相对底层的 “plumbing” 管道命令)。

可以译作“高层瓷器层”或更通俗的“高层用户界面 / 工具”。

因为 Git 作者 Linus 用建筑管道的比喻:底层功能像“plumbing”(管道、阀门),提供原始、细粒度的操作;

面向用户的友好命令是装在外面的“porcelain”(马桶、洗手池的瓷面部分),把粗犷的管道包装成可用的界面。

Git 把自己的高层子命令(如 git commit, git status)称作 porcelain,把内部原语(如 hash-object, update-index)称作 plumbing。

Magit 作为更友好的界面,也自称“Git porcelain”。

magit-base.el

User Input

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-base.el>

magit-mode.el

Buffer-local variables (magit-mode.el)

[2025-12-30 Tue]

(defvar-local magit-buffer-arguments nil)
(defvar-local magit-buffer-diff-type nil)
(defvar-local magit-buffer-diff-args nil)
(defvar-local magit-buffer-diff-files nil)
(defvar-local magit-buffer-diff-files-suspended nil)
(defvar-local magit-buffer-file-name nil)
(defvar-local magit-buffer-files nil)
(defvar-local magit-buffer-log-args nil)
(defvar-local magit-buffer-log-files nil)
(defvar-local magit-buffer-range nil)
(defvar-local magit-buffer-range-hashed nil)
(defvar-local magit-buffer-refname nil)
(defvar-local magit-buffer-revision nil)
(defvar-local magit-buffer-revision-hash nil)
(defvar-local magit-buffer-revisions nil)
(defvar-local magit-buffer-typearg nil)
(defvar-local magit-buffer-upstream nil)

这些 defvar-local 变量的作用是:用 buffer-local 状态记录“这个 Magit buffer 当前在展示什么”,

从而让 refresh、生成 git 命令参数、以及 buffer 的复用/锁定(magit-get-mode-buffer / magit-buffer-value)能工作。

  • 定义位置:magit-mode.el: Local Variables

  • 设置入口:magit-setup-buffer / magit-setup-buffer-internal

  • 复用/锁定:magit-get-mode-buffer / magit-toggle-buffer-lock

  • magit-buffer-diff-files: diff 的“文件过滤器”(只对这些文件生成 diff)。

  • magit-buffer-files: 在这份源码树里只有定义,没有任何引用点(未被读取/设置),所以 core 里当前不生效。

  • =magit-buffer-revision=(变量):该 buffer 关联的单个 revision/对象;revision buffer、stash buffer、以及“访问某个 revision 的文件内容”的文件 buffer 会用到它。

  • =magit-buffer-revision=(函数):magit-buffer-revision

    • 若变量非 nil 则直接返回。
    • 若变量为 nil 但当前 buffer 在访问一个被跟踪的文件,则返回 ={worktree}=;否则返回 nil。
  • magit-buffer-typearg: diff 的“类型参数”(常见如 --cached=、—no-index=),会进入 git diff/show 的参数构造与标题行逻辑。

  • magit-buffer-range: diff/log/cherry 等 buffer 的 revision range 字符串(如 =A..B=、=A…B=、=rev^..rev=),用于实际 git 调用参数,也常作为该 mode 的 buffer value(用于锁定/复用)。

其它常见变量(同样都是“描述当前 buffer 展示内容/参数”的元数据):

  • magit-buffer-arguments: 某些 mode 的通用参数列表(目前主要用于 =magit-refs-mode=)。
  • magit-buffer-diff-args / magit-buffer-diff-type
  • magit-buffer-log-args / magit-buffer-log-files / magit-buffer-revisions
  • magit-buffer-refname / magit-buffer-upstream
  • =magit-buffer-file-name=(用于 blob/index/revision 的文件访问 buffer;因此被标记 permanent-local)
  • =magit-previous-section=(刷新前后尽量回到同一 section;见 magit-log-goto-same-commit
  • magit-buffer-revision vs magit-buffer-refname

    [2025-12-30 Tue]

    两者经常存放相似的 rev 字符串,但语义不同:

    • magit-buffer-revision: 更偏“对象/提交”(commit-ish),用于表示“这个 buffer 正在看哪个 commit/stash/对象”。

      例如:

      revision/stash buffer 会把它当作 show/diff 的目标(magit-revision-setup-buffermagit-stash-setup-buffer)。

    • magit-buffer-refname: 更偏“引用名”(ref 名,如 branch/tag/refs/stash),以及一些特殊占位(例如 ={index}=)。

      例如:

      stash 列表把它固定为 =refs/stash=(magit-stashes-setup-buffer),

      reflog buffer 用它表示“哪个 ref 的 reflog”(magit-reflog-setup-buffer)。

    一个很直观的差异点在“访问某个 revision 的文件内容”的 buffer:

Refresh Buffers (magit-mode.el)

[2025-12-31 Wed]

codex resume 019b7558-db8f-78a0-8db5-ffae1b356dba

Magit 的刷新分两层:

  • 仓库级统一入口:=magit-refresh=
    • 刷新“当前 buffer(若是 Magit buffer)”,并按需刷新“对应仓库的 status buffer”。
    • 在一次刷新周期内提供统一的 hook/caching/异常收尾语义。
  • buffer 级统一入口:=magit-refresh-buffer=
    • 负责“如何刷新一个 Magit buffer”的通用框架:清理状态、(尽量)保留光标/窗口位置、重建内容、恢复高亮。
    • 具体内容由各个 mode 的 xxx-refresh-buffer 实现。
(defun magit-refresh ()
  (unless magit-inhibit-refresh
    (unwind-protect
        (let ((magit--refresh-cache (or magit--refresh-cache
                                        (list (cons 0 0)))))
          (magit-run-hook-with-benchmark 'magit-pre-refresh-hook)
          (cond ((derived-mode-p 'magit-mode)
                 (magit-refresh-buffer))
                ((derived-mode-p 'tabulated-list-mode)
                 (revert-buffer)))
          (when-let ((buffer (and magit-refresh-status-buffer
                                  (not (derived-mode-p 'magit-status-mode))
                                  (magit-get-mode-buffer 'magit-status-mode))))
            (with-current-buffer buffer
              (magit-refresh-buffer)))
          (magit-run-hook-with-benchmark 'magit-post-refresh-hook))
      (run-hooks 'magit-unwind-refresh-hook))))
 
(cl-defun magit-refresh-buffer (&optional created &key initial-section select-section)
  (when-let ((refresh (magit--refresh-buffer-function)))
    ;; ... 清理 section/overlay 状态,必要时保存并恢复 positions ...
    (funcall refresh)
    (run-hooks 'magit-refresh-buffer-hook)))
 
(defun magit--refresh-buffer-function ()
  (let ((fn (intern (format "%s-refresh-buffer"
                            (substring (symbol-name major-mode) 0 -5)))))
    (and (functionp fn)
         (lambda ()
           (let ((inhibit-read-only t))
             (erase-buffer)
             (save-excursion (funcall fn)))))))

为什么需要统一的 refresh 接口(=magit-refresh=)?

  • 避免每个“有 git 副作用”的命令都手写“该刷哪些 buffer”的逻辑:统一定义为“当前 buffer(若适用)+ status buffer(可配置)”。相关开关:magit-refresh-status-buffer
  • 统一跨 buffer/跨模块的副作用点:=magit-pre-refresh-hook= / magit-post-refresh-hook 用于“与具体 buffer 无关”的工作(例如保存相关文件 buffer、auto-revert、post-commit/stage/unstage hook 等)。见 magit-pre-refresh-hookmagit-post-refresh-hook
  • 统一刷新周期内的缓存边界:刷新时 let-bind magit--refresh-cache=,让大量 =magit-git-* 查询可在一个 refresh 周期内复用(通过 =magit—with-refresh-cache=)。见 magit-refreshmagit—refresh-cachemagit—with-refresh-cache
  • 统一异步/同步执行后的 UI 落点:
    • 同步 side-effect:=magit-run-git= 在 git 返回后只需要调用一次 =magit-refresh=。magit-run-git
    • 异步 side-effect:process sentinel 在子进程结束时调用 magit-refresh=,并支持通过进程属性 =inhibit-refresh 禁止自动刷新。magit-process-sentinel
  • 统一异常收尾:=unwind-protect= 确保即使刷新中途报错也会运行收尾 hook(=magit-unwind-refresh-hook=)。magit-refresh

不同类型 buffer 如何控制刷新?

magit-log.el

我们有 magit-diff-toggle-file-filter 但是我们没有 magit-log-toggle-file-filter !

这是为什么?为什么 diff 需要但是 log 不需要呢?

magit-log-buffer-file

SPC g L

Show log for the blob or file visited in the current buffer.

这东西太tm好用了

相当于收集了一个文件的所有相关 commit

加上前缀就是不聚焦与单个文件,就是拉入所有相关 commit 能看到其他文件的修改

在选定region时调用就是在运行 git log -L

magit-files.el

非常重要的 magit-blob-mode

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-files.el>

File Dispatch

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-files.el>

magit-find-file 开头就是最核心的 打开特定 rev 下的 file

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-files.el>

默认是用 pop-to-buffer-same-window 然后也提供了 magit-find-file-other-window 使用 switch-to-buffer-other-window 跳转

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-files.el>

magit-find-file-noselect 没看懂这是干什么用的

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-files.el>

我应该需要先理解 find-file-noselect 的功能

elisp#Visiting Functions

magit-diff.el

架构总览

  • magit-diff.el 是 Magit 的 diff 中枢:包含 diff/revision/merge-preview 三类视图的 major mode、diff 输出解析

    (wash)、以及参数系统/瞬态菜单的 glue code。
  • 所有 diff 视图都建立在 magit-section 树上:先把 git diff 的原始文本插入 buffer,再通过 wash

    阶段切成 diffstat -> file -> hunk 的层级结构,后续的导航/操作都依赖 section 的 value 和 marker。
  • 关键入口集中在 diff/revision 的 autoload 命令和对应的 refresh(例如 magit-diff / magit-diff-refresh=、 =magit-show-commit / =magit-revision-refresh=)。

数据流 (command -> buffer)

  1. 命令入口(=magit-diff=、=magit-show-commit= 等)先计算 diff 参数:

    magit-diff-arguments -> magit-diff—get-value
  2. 插入阶段:=magit-diff-insert=/=magit-insert-diff= 通过 magit-git-wash 执行 git diff,把输出插入到 section。
  3. wash 阶段:magit-diff-wash-diffs 先处理 diffstat,

    再逐个解析 file/hunk(=magit-diff-wash-file=/=magit-diff-wash-hunk=)。
  4. UI 层:section tree 决定了可见性、折叠、跳转、以及 stage/discard/ediff 等操作的上下文。

Section 结构与类型

  • diffstat: diff 的统计区块,子节点是每个文件的 stat 行,后面紧接真实 diff。
  • file section: type file / =magit-file-section=,value 是路径;用于跳转、定位 hunk,以及聚合操作。
  • hunk section: type hunk / =magit-hunk-section=,保存 hunk header + body;精修/高亮都在这一层完成。
  • revision buffer: magit-revision-mode 把 commit header/metadata 和 diff sections 拼在一起,复用 diff wash 逻辑。

参数与 transient 机制

  • diff prefix 定义了 args + files 的 value 结构,并把 history/major-mode 绑定到 diff buffer。
  • magit-diff—get-value 是核心决策树:
    • 根据 magit-*-use-buffer-arguments 复用 buffer-local 的 =magit-buffer-diff-args/files=。
    • 若没有可用 buffer,则读取 mode 属性 =magit-diff-current-arguments=。
    • 再回退到 transient-values 里的 =magit-diff:<mode>=。
    • 最后使用 =magit-diff-default-arguments=。
  • magit-diff—set-value 把 transient 选择写回 buffer-local

    和 transient-values,并触发 =magit-refresh=,保证 prefix/status/direct 三个入口共享同一套 args。

刷新策略

  • diff buffer 的 revert-buffer-function 指向 =magit-diff-refresh=,每次刷新都复用 buffer-local args/files。
  • context/whitespace 等开关会在刷新前改写参数(例如 magit-diff-context-set 会更新 args 并刷新)。
  • status/log buffer 中的 diff sections 也是基于 buffer-local args,因此改参数后刷新即可同步。

Hunk 细节处理

  • refine:=magit-diff-refine-hunk= + magit-diff-update-hunk-region 用 overlay 做 word-level 差异。
  • whitespace paint:=magit-diff-paint-whitespace= 系列函数负责 trailing/tab 等标记,按阈值决定是否启用。
  • visit/dwim:=magit-diff-visit-file= / magit-diff-visit-worktree-file 根据是否 revision buffer 决定打开 blob 还是 worktree 文件。

详细分析

  • 数据流总览

    transient args
      -> buffer-local state (magit-buffer-*)
      -> magit--insert-diff (git diff/show/merge-tree)
      -> magit--git-wash
         -> magit-diff-wash-diffs
            -> magit-diff-wash-diff (file/module section)
               -> magit-diff-wash-hunk (hunk sections)
      -> magit-section-paint / refine / region overlays
  • 参数入口与 buffer-local 状态

    • 参数入口是 transient 前缀 magit-diff / magit-diff-refresh=,对应类 =magit-diff-prefix / =magit-diff-refresh-prefix=。

      关键点:transient 的 value 在这里被规整成 (args files) 二元组,并写回 buffer-local 变量。参见

      magit-diff-prefix,

      transient-prefix-value,

      magit-diff—set-value
    • 关键 buffer-local 变量:
      • =magit-buffer-range=:范围字符串 (e.g. “A..B”, “A…B”)。
      • magit-buffer-typearg=:—cached= / --no-index 等。
      • =magit-buffer-diff-args=:diff 选项集合(transient 输出)。
      • =magit-buffer-diff-files=:路径过滤。
      • =magit-buffer-diff-type=:显式指定类型时覆盖推断。
      • =magit-buffer-range-hashed=:refresh 时缓存 range hash,用于对比和优化。参见 magit-diff-refresh-buffer
    • magit-diff--get-value 决定“取当前 buffer 的参数还是保存的默认值”,依据

      magit-prefix-use-buffer-arguments / magit-direct-use-buffer-arguments 等策略。参见 magit-diff—get-value
  • diff 生成与命令拼装

    • magit-insert-diff 组装 git 调用:“diff” + range + “-p” + “—no-prefix” + (必要时) “—numstat” + typearg + args + files。

      magit-insert-diff
    • magit--insert-diff 做真正的“命令修正”:
      • 强制加入 =—ita-visible-in-index=(除了 merge-tree)以支持 intent-to-add 文件。
      • 如果存在 --stat 则注入 =magit-diff-extra-stat-arguments=。
      • 若启用 --color-moved=,强制 =--color=always 并重置部分 color 配置,避免 context 被 dim。随后 wash 里会调用

        =ansi-color-apply-on-region=。参见

        magit—insert-diff
      • --no-index 的 diff 使用 =wash-anyway=,允许非零退出码仍展示 diff。
  • wash 解析流程 (diff text -> section tree)

    • =magit-diff-wash-diffs=:

      • 先运行 hook:=magit-diff-wash-diffs-hook=。
      • 若带 --show-signature 或 =—stat=,先处理签名与 diffstat。
      • 之后用 magit-wash-sequence 反复调用 =magit-diff-wash-diff=,每个 diff 后插入空行。

      参见 magit-diff-wash-diffs

    • magit-diff-wash-diff 是核心 parser:

      • 处理 submodule summary、”* Unmerged path” 和 merge-tree 的冲突 headline。
      • 对 “diff —git""diff —cc""diff —combined” 解析 extended headers:

        old/new mode, new/deleted file, rename/copy, similarity, index, ---/+++ 路径、binary。
      • diff --git 行的路径可能带空格,所以最终文件名以 ---/+++ 行为准。
      • git log -L 场景会忽略 =—no-prefix=,Magit 在这里重写 header 以保证后续解析一致。
      • 最后调用 magit-diff-insert-file-section 建立 file/module section。

      参见 magit-diff-wash-diff

    • =magit-diff-insert-file-section=:

      • 新建 (file …) section,设置 :source/:header/:binary 等 slot。
      • 先插入 heading (走 magit-format-file-function),再把 chmod/rename 当作特殊 hunk 插入。
      • 最后继续 magit-wash-sequence 调 =magit-diff-wash-hunk=。

      参见 magit-diff-insert-file-section

    • =magit-diff-wash-hunk=:

      • 解析 @@/@@@ 头,构造 range 列表。
      • combined diff 的 hunk 会有 3 个 range (ours/base/theirs)。
      • 把 ranges 写入 :from-range / :to-range / :combined / :about。

      参见 magit-diff-wash-hunk

  • section 模型与核心字段

    • diff 相关的 section class 不在 magit-diff.el,而在 magit-base.el:

      • magit-diff-section (abstract)
      • =magit-file-section=:slot 有 :source/:header/:binary
      • =magit-module-section=:submodule 专用
      • =magit-hunk-section=:slot 有 :combined/:from-range(s)/:to-range/:about/:painted/:refined

      参见 magit-diff-section,

      magit-file-section,

      magit-hunk-section

    • section type -> class 的映射在 magit--section-type-alist 中注册(file/module/hunk)。参见

      magit—section-type-alist

  • 渲染/细化/区域高亮

    • magit-section-paint 针对 hunk 行逐行着色:

      • 根据行首 +/-/space 选择 added/removed/context face。
      • combined diff 用不同的前缀处理,冲突标记 (<<<<<<< etc) 走专用 face。
      • 读取/缓存 tab-width,并可对 tab 做显示宽度替换。
      • 可选 whitespace 高亮(trailing/indent),用 overlay 标记。

      参见 magit-section-paint

    • refine 逻辑:

      • magit-diff-refine-hunk 为 nil/t/all;t 表示只 refine 当前 hunk。
      • magit-section--refine 在切换 section 时触发。
      • combined diff (@@@) 会跳过 =diff-refine-hunk=。

      参见 magit-diff-update-hunk-refinement

    • hunk 内部 region 高亮:

      • magit-diff-update-hunk-region 先插入 header overlay,再执行

        magit-diff-highlight-hunk-region-functions 的各个策略。
      • 在 TTY 下会降级为 face 高亮。

      参见 magit-diff-update-hunk-region

    • 展开阈值:

      • refresh 太慢时,=magit-diff-expansion-threshold= 会让新 file section 默认折叠。

      参见 magit-diff-expansion-threshold

  • 语义判定与交互命令 (apply/stage/visit)

    • magit-diff-type 依据 mode/range/typearg 推断 staged/unstaged/committed/undefined。

      不同 diff type 决定哪些命令可用(menu/keymap 的 :enable 条件)。

      参见 magit-diff-type

    • magit-diff-scope 决定选择的是 region/hunk/hunks/file/files/list。

      这直接影响 apply/stage/unstage/discard 的 scope 与参数。参见

      magit-diff-scope

    • 访问文件/行号映射:

      • =magit-diff-visit-file* -> magit-diff-visit-file—internal -> magit-diff-visit-file—noselect=。
      • magit-diff-visit--sides 把 diff type 映射到 (old-rev . new-rev),例如 staged: HEAD vs {index},

        unstaged: {index} vs {worktree}。
      • magit-diff-visit--position 用 hunk ranges + diff offset 算行号,

        magit-diff-visit--offset 会额外跑一次 git diff 来修正跨多次变更的行偏移。

      参见 magit-diff-visit—sides, magit-diff-visit—offset

  • diffstat 解析与格式化

    • 当 diff args 含 --stat 时,Magit 还会加上 --numstat=,用于建立文件列表与统计。 wash 阶段解析 numstat 行并结合 =magit-diff-statline-re 生成 diffstat section。

      参见 magit-diff-wash-diffstat
    • diffstat 的 file heading 也走 =magit-format-file-function=,因此图标/样式在这里统一。
  • 扩展点/可定制点

    • =magit-diff-sections-hook=:决定 diff buffer 插入哪些 section。
    • =magit-diff-wash-diffs-hook=:在解析前做预处理。
    • =magit-diff-highlight-hunk-region-functions=:控制 hunk 内部 region 强调策略。
    • =magit-format-file-function=:统一文件 heading 的格式。
    • =magit-diff-extra-stat-arguments=:为 diffstat 注入额外参数(例如动态 stat-width)。

magit-diff-section

magit-hunk-section

(defclass magit-hunk-section (magit-diff-section)
  ((keymap      :initform 'magit-hunk-section-map)
   (painted     :initform nil)
   (fontified   :initform nil) ;TODO
   (refined     :initform nil)
   (combined    :initform nil :initarg :combined)
   (from-range  :initform nil :initarg :from-range)
   (from-ranges :initform nil)
   (to-range    :initform nil :initarg :to-range)
   (about       :initform nil :initarg :about)
   (heading-highlight-face :initform 'magit-diff-hunk-heading-highlight)
   (heading-selection-face :initform 'magit-diff-hunk-heading-selection)))
  • painted / refined(你已经有)
  • fontified(Magit 用于细粒度字体处理;可选)
  • combined(是否是 combined diff,例:@@@)
  • from-range / to-range(左右两侧的行号+长度)
  • from-ranges(combined diff 时多条 from-range)
  • about(@@ … @@ 后面的描述)
  • heading-* faces(你已有)

magit-diff-insert-file-section 这是非常值得参考的函数

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

diff section 也是有类型的!

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

有 staged unstaged committed undefined 四种

这里在得到 diff 中文件的修改类型

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

magit-diff transient

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

不知道为为什么还有

transient-define-group magit-diff-infix-arguments (infix 又是什么东西?)

transient-define-prefix magit-diff-refresh (refresh 还需要选项我是没搞明白)

还有很多 transient-define-argument 这我之前是没写的

magit-diff-dwim diff的集中入口

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

还有

magit-diff-range

magit-diff-paths

magit-diff-unstaged

magit-diff-staged

magit-diff-working-tree

magit-show-commit

magit-stash-show

为什么需要有 magit-diff-refresh?

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

关于 hunk paint

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

Magit 的 hunk paint(magit-section-paint 针对 magit-hunk-section,见 magit/lisp/magit-diff.el 约 3338–3409)主要做几件事:

  • 决定是否用“高亮版”配色:如果 magit-diff-highlight-hunk-body 关闭,则把传入的 highlight 置空;否则用 *_highlight faces。
  • 识别当前 hunk 是否是 combined diff(@@@),获取 diff 类型(staged/unstaged/committed)以及计算文件的实际 tab 宽度(magit-diff-tab-width),初始化冲突阶段标记 stage。
  • 从 hunk header 下一行开始逐行:
    • 可选地把行尾的 \r 设为 invisible(magit-diff-hide-trailing-cr-characters)。
    • 根据行首字符决定 face:
      • 冲突分隔线(++<<<<<<< 等)会设置 stage,并用 conflict heading 的普通/高亮面。
      • “+” 行:先对 tab 做等宽 display,再调用 magit-diff-paint-whitespace 标记空白,再用 added/added-highlight(或当前 conflict 阶段的 face)。
      • “-” 行:同上,用 removed/removed-highlight。
      • 其他(空格开头)行:处理 tab/空白后,用 context/context-highlight。
  • 如果用户把 magit-diff-refine-hunk 设为 all,paint 结束立刻调用 magit-diff-update-hunk-refinement,对整段做词级 diff。
  • 最后把 section 的 painted 槽标为 highlight 或 plain,让后续 highlight 逻辑知道已经 paint 过。

简而言之:paint 负责“给 hunk 各行着色 + 处理 tab/空白/CR +(可选)自动行内 refine”,并记录 painted 状态;行内细化真正靠后续的 diff-refine-hunk 完成。

magit-ediff.el

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-ediff.el>

除了 ediff 支持 还有 smerge 支持

magit-worktree

link

Worktree (Magit User Manual)

magit-repos

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-repos.el>

magit-list-repositories

还有一个专门的 evil-collection-magit-repos 给这东西提供功能

因为这东西提供了不基于 magit-section 的 UI

需要先配置 magit-repository-directories

git-process

我现在的理解是把 emacs 的 process 组件给封装了一遍

magit-process ATTACH

设计目标与分层

Magit 把“跑 git 命令”拆成两条主路径:

  • “为副作用而运行”(run for side-effects):

    统一记录输出到 =*magit-process*=,用默认 filter/sentinel 处理交互/提示/错误,并在结束后触发 refresh;调用点通常只关心 exit code,不需要单独的回调。
  • “为解析输出而运行”(run for parsing):

    仍然使用 Emacs process,但把 filter/sentinel 的控制权交给具体功能模块(例如 blame 需要增量解析),

    模块通过覆写 filter/sentinel 来驱动自己的状态机。

对应到实现里就是:

数据流: start -> filter -> finish -> refresh

以默认的异步路径为例(magit-start-process ):

  • 先创建一个新的 process section 作为这次命令的“日志条目”(magit-process-setup / magit-process-insert-section ),并把 section 挂到 process 上(process-put ‘section)(magit/lisp/magit-process.el:758, 766, 710)。
  • 启动进程时用 =start-file-process=(而不是 =start-process=),并设置 =process-environment=、pty/pipe、coding system 等,让 Tramp/Windows 这类场景也能工作

    (magit/lisp/magit-process.el:662-709, 528-546)。
  • 默认 filter 把输出追加到 process buffer,并做“终端语义修复”与“交互提示转发”:
    • 处理 carriage return 进度条(^M)覆写同一行;
    • 去掉一些已知的伪错误文本;
    • 可选地在 filter 阶段或结束阶段应用 ANSI 颜色;
    • 检测 yes/no, username, password 以及用户自定义 prompt,并把回答发回子进程(magit/lisp/magit-process.el:879-900)。
  • 默认 sentinel 在进程退出时负责“收口”:
    • 调用 magit-process-finish ,更新 section heading 的 exit code、提取错误摘要并写入 mode-line/状态缓冲的 =magit-this-error=;
    • 清理 =magit-this-process=;
    • 触发 refresh(优先回到启动命令时的 command buffer,否则用 default-dir 临时 refresh)

      (magit/lisp/magit-process.el:840-856, 1240-1275, 1277-1303)。

这里“回调”并不是消失了,而是被收敛进默认 sentinel/filter: 对“副作用命令”来说,真正的“结果”就是 repo 状态变化,Magit 用 refresh 重建 UI;个别需要特殊收尾/增量解析的功能,则直接替换 filter/sentinel(例如 blame)而不是让每个调用点都传 callback 参数。

进程元数据: process-put + section slot

Magit 用两套机制把运行时上下文串起来:

  • Emacs process 上的属性(process-put / process-get):
    • ‘section: 对应的 magit-process-section
    • ‘command-buf: 启动命令时所在的 buffer
    • ‘default-dir: 用于 refresh、dired uncache 等
    • ‘inhibit-refresh: 禁止 sentinel 自动 refresh(例如批处理/内部调用)
    • magit-parse-git-async 里还会有 ‘stderr-buf, ‘parsed(magit/lisp/magit-process.el:710-714, 748-750)。
  • section 的 slot:
    • magit-process-section 继承 magit-section=,额外有一个 =process slot;异步启动时把 process 写到 (oref section process),并把 (oref section value) 也设成 process,方便在 process buffer 里“点到哪里就能拿到 process”(magit/lisp/magit-process.el:47-48, 715-729)。

这种设计让 “UI = sections” 和 “执行 = process objects” 能互相索引:

既能在输出文本中定位所属 section(输出文本也被 propertize 了 ‘magit-section),也能反过来从 section 一键 kill/process 操作。

同步 vs 异步: 统一日志,不同的控制点

  • 同步(magit-call-process / magit-call-git):
    • 直接用 magit-process-file / process-file 把输出写进 process buffer 的 section,然后调用 magit-process-finish 做同样的收口逻辑;
    • 调用点拿到 exit code 继续执行,所以一般不需要回调(magit/lisp/magit-process.el:493-503, 515-530, 1240)。
  • 异步(magit-start-process):
    • 主要差异是“输出到达时”的 filter 负责交互提示,以及 sentinel 负责结束后的 refresh;
    • 若要自定义行为,不是传 callback,而是替换 filter/sentinel 或在 process 上挂更多状态(magit/lisp/magit-process.el:662-730, 879-900, 840-856)。

magit-run-git 同步运行

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-process.el>

magit-run-git-async 异步运行

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-process.el>

关于 magit-process-section

一个 magit-process-section 的例子:


magit-process-section 是 Magit 的“进程日志条目”section:

每跑一次 Git(同步/异步),在 *magit-process* 里就插入一个 process section 来承载这次命令、输出和状态。

magit/lisp/magit-process.el:47

magit/lisp/magit-process.el:756

关键点:

  • 异步启动时 magit-start-process 把 Emacs 的 process 对象同时写进 section 的 (oref section process) 和通用的 (oref section value):

    前者主要给内部逻辑用(比如截断旧日志时判断“还在跑就别删”),

    后者让通用 API(magit-section-value-if ‘process)能在 point 上拿到进程(例如 magit-process-kill)。

    magit/lisp/magit-process.el:662

    magit/lisp/magit-process.el:715

    magit/lisp/magit-process.el:728

    magit/lisp/magit-process.el:408

    magit/lisp/magit-process.el:820

  • 进程输出通过 filter 插入时,会把插入的文本都标上同一个 magit-section(从 process-get proc ‘section 取),

    所以光标在输出里也能“属于”这次运行的 section。

    magit/lisp/magit-process.el:879

  • 结束时 magit-process-finish(-section) 会把 heading 里的 “run” 改成退出码、可选地上 ANSI 颜色/自动折叠等。

    magit/lisp/magit-process.el:1240

    magit/lisp/magit-process.el:1277

git-commit

eldoc-mode

它的功能是搞一些高亮出来

Major mode git-commit-elisp-text-mode

The major mode is ElText mode defined in git-commit.el:

Major mode for editing commit messages of elisp projects.

This is intended for use as ‘git-commit-major-mode’ for projects

that expect ‘symbols’ to look like this. I.e., like they look in

Elisp doc-strings, including this one. Unlike in doc-strings,

“strings” also look different than the other text.

In addition to any hooks its parent mode ‘text-mode’ might have run,

this mode runs the hook ‘git-commit-elisp-text-mode-hook’, as the

final or penultimate step during initialization.

magit-apply

比较重要的一个功能是选择 hunk 的一部分来操作(stage 之类的)

intro

Magit 自己“拼”一份只含你选中行的补丁,再用 git apply 去更新索引/工作区,而不是依赖 =git add -p=。核心流程(见 lisp/magit-apply.el):

  1. 选区获取
  2. 拼补丁片段
  3. 调用 git apply
    • 补丁传给 magit-apply-patch ,用 magit-run-git-with-input “apply” … ”-” 执行。
    • 加的参数决定“作用域”:
      • stage 选区:—cached
      • unstage:—reverse —cached
      • 仅工作区:无 —cached
    • 其他默认参数:-p0, -C<context>, —ignore-space-change,其中 <context> 来自当前 diff 上下文设置,确保有足够的 context 匹配。
  4. 刷新与钩子
    • 成功后触发 magit-refresh、magit-post-stage-hook 等,保持 UI 与索引同步。
    • WIP 模式会在 apply 前后自动提交快照。

精髓:Magit 先把“选中行”转成最小可用补丁,再用 git apply —cached/-‐reverse 等组合,把索引/工作区改到目标状态,这就是它能精确 stage hunk 子集的原因。

section 选择逻辑

简答:Magit 基本不支持“跨类型 section 混选再 apply”。它把“可选区域”限制为同级 sibling 的 heading,并且要求所有选中 section 都匹配同一个条件集合;否则就视为无效选择,命令退回到“只处理 point 处的 section(或报错)”。

关键点(带位置):

  • 选区有效性的核心约束:region 必须从一个 section 的 heading 起,到同一层级的 sibling heading 结束;否则 magit-region-sections 返回 nil。

    magit/lisp/magit-section.el:2161
  • 另外还要求“所有选中 sections 都满足 CONDITION”;不满足同一套条件就视为无效。

    magit/lisp/magit-section.el:2161
  • magit-apply—get-selection 在 apply 类命令里调用 (magit-region-sections ‘(hunk file module) t),也就是只允许 hunk/file/module 这三类,并且要求多选。

    magit/lisp/magit-apply.el:242
    • 由于 region 只能选择 sibling,实际上你要么选多个 hunks(同一 file 下的 siblings),要么选多个 files(diff-root 下的 siblings),但不会混。

所以“region 里有多个类型的 section”时,Magit 的行为是:

  • 这类 region 通常被判定为无效选择;
  • 命令会回落到 point 处的 section,或者报 “Cannot apply this, it’s not a change”。

    magit/lisp/magit-apply.el:242

如果你想在 Majutsu 里支持“跨类型选区”,通常有两条路线:

  1. 严格模式(与 Magit 一致)

    直接判为无效选择,提示用户只选同类(hunk 或 file)。
  2. 宽松模式(自行实现)

    自己扫区域内的 sections(遍历并取 heading 落在 region 内的 section),然后:
    • 先按类型过滤(只收 hunk/file)
    • 再按 parent 分组(同一 file 的 hunks 一起 apply)
    • 分批调用 apply(hunks 走 hunk apply,files 走 file apply)

如果你愿意,我可以帮你在 Majutsu 里实现这种“宽松模式”的选区解析逻辑。


简答:除了 “hunk 内部 region” 以外,Magit 的 region 只在 section heading 级别 生效。它不会在其他 section 内部做“部分 patch”。一旦 region 不是“合法的 section 选择”,就退回到 point 处的单一 section(或直接无效)。

关键实现点在 magit-region-sections 和 magit-apply—get-selection。

magit/lisp/magit-section.el:2161、magit/lisp/magit-apply.el:242

具体分情况:

  1. 选中多个 hunk(同文件下的 sibling hunks)

    • region 必须从一个 hunk heading 到另一个 sibling hunk heading。
    • 返回 hunks 列表,magit-diff-scope 为 hunks,走 magit-apply-hunks。
    • 这会拼出一个包含多个 hunk 的 patch(按需调整 header)。

      magit/lisp/magit-apply.el:182
  2. 选中多个 file section(diff 根下的 sibling files)

    • region 必须在 file headings 之间。
    • 返回 files 列表,magit-diff-scope 为 files/list,走 magit-apply-diffs。

      magit/lisp/magit-apply.el:138
  3. 选中 diffstat 里的 file entries

    • diffstat 里的条目也是 file section。
    • 选中后会通过 magit-apply—get-diffs 把 diffstat 条目映射成真正的 diff file section,再 apply。

      magit/lisp/magit-apply.el:253
  4. 选中 module section

    magit-apply—get-selection 允许 module 类型,但处理方式仍然是“按 file/section 级别拼 patch”,不会有“module 内部部分 patch”。

    magit/lisp/magit-apply.el:242

  5. 混合类型 / 非 heading 的 region

    • 例如跨 file + hunk、或选在 section body 内(非 heading),都会被判定为 无效 selection。
    • 命令会退回到 point 处的 section(或直接不执行)。

    magit/lisp/magit-section.el:2161、magit/lisp/magit-apply.el:242

结论

只有 hunk 内部 region 有专门的“部分 patch”逻辑;

其他类型的 region 只是“选中若干 section”,并且必须是同级 sibling 的 headings。

如果你想在 Majutsu 里做“跨类型 region 混选”,那就是要自己实现:

  • 扫描 region 内所有 section headings
  • 按类型/parent 分组
  • 分批调用 hunk/file apply

我可以帮你设计这套逻辑。

hunk region

<~/.config/emacs/.local/straight/repos/magit/lisp/magit-diff.el>

简要答案:Magit 把 “hunk 内部的选区” 转成一个“只包含选中改动”的新 hunk patch,然后再用 git apply 应用。

关键流程(按调用链):

  1. 识别 hunk 内部 region

  2. region 会被对齐到整行边界(BOL/EOL):

    magit-diff-hunk-region-beginning / magit-diff-hunk-region-end

    magit/lisp/magit-diff.el:3519

  3. 生成“局部 hunk patch”

  4. magit-apply-region 调用 magit-diff-hunk-region-patch

    magit/lisp/magit-apply.el:206

    magit/lisp/magit-diff.el:3661

  5. magit-diff-hunk-region-patch 扫描整个 hunk 的每一行,按规则拼 patch:

    • 总是保留 hunk header(@@)和 context 行(前缀空格)。
    • 在选区内的 +/- 行原样保留。
    • 不在选区内的行:
      • 若前缀等于 op(普通 apply 时 op = ”-“,reverse 时 op = ”+”),就把该行 转成 context 行(前缀改成空格)。
      • 其余行直接丢弃。
    • 这样就“中和”了未选中的那一边,同时避免要求新加入的行已存在。

      magit/lisp/magit-diff.el:3661
  6. 修正 hunk header

  7. 在临时缓冲里调用 diff-fixup-modifs 重新计算 @@ -a,b +c,d @@ 的计数。

    magit/lisp/magit-diff.el:3661

  8. magit-apply-region 还会用 magit-apply—adjust-hunk-new-start 调整 +start,避免部分应用导致行号偏移。

    magit/lisp/magit-apply.el:179、magit/lisp/magit-apply.el:206

  9. 应用

  10. 生成的 patch 交给 magit-apply-patch(最终是 git apply)。

    magit/lisp/magit-apply.el:216

file region

magit-blame

magit-blame

SPC g B

类似的还有 vc-annotate

并不支持 -L,可能的确是不太重要?