GitButler CLI 核心概念深度解析:并行工作区模型、短 ID 系统与历史编辑体系
2026/9/13 18:46:50 网站建设 项目流程

GitButler CLI 核心概念深度解析:并行工作区模型、短 ID 系统与历史编辑体系

【免费下载链接】gitbutlerThe GitButler version control client, backed by Git, powered by Tauri/Rust/Svelte项目地址: https://gitcode.com/GitHub_Trending/gi/gitbutler

本篇技术指南以 GitButler CLI(but命令)的核心概念文档 concepts.md 为主线,深入讲解 GitButler 与 Git 截然不同的工作区模型(并行栈替代串行分支)、会话级 CLI 短 ID 系统、基于 Sources/Target 的历史编辑模型、依赖跟踪、操作日志(Oplog)与冲突解决模式。读完本文,你将掌握 GitButler CLI 的设计哲学与底层运行机制,能够正确阅读but status/but diff输出并安全使用but commitbut squashbut movebut resolve等核心命令。

工作区模型:从串行分支到并行栈

传统 Git 的串行分支

在传统 Git 中,分支是互斥的工作上下文。一次只能在一个分支上工作:

main ──┬── feature-a (checkout here, work, commit, checkout back) └── feature-b (checkout here, work, commit, checkout back)
  • 同一时间只能在一个分支上工作;
  • 切换上下文必须执行git checkout
  • 变更通过"分支隔离"实现,未提交的改动随 checkout 一起切换。

这种模型的问题在于:当你同时进行多个任务(比如修复线上 bug 与开发新功能)时,需要频繁地在上下文之间切换,git stash、临时分支成了日常操作,很容易丢失或混淆工作现场。

GitButler 的并行栈

GitButler 彻底改变了这个模型。所有"已应用(applied)"的分支同时存在于工作目录中,通过一个gitbutler/workspace合并提交整合在一起:

workspace (gitbutler/workspace) ├─ feature-a (applied, merged into workspace) ├─ feature-b (applied, merged into workspace) └─ feature-c (unapplied, not in workspace)
  • 可以同时在多个分支上工作;
  • 无需上下文切换——所有已应用分支的改动都合并在工作目录中;
  • 变更通过**分配(ASSIGNED)**给分支来归属,而不是通过 checkout 隔离。

三个关键推论

  1. 不再需要git checkout:你不需要在分支之间切换。所有已应用分支同时存在于工作区中,编辑的文件通过 CLI ID 显式分配给目标分支。

  2. gitbutler/workspace分支是内部实现细节:它是一个包含所有已应用栈的合并提交。不要直接操作它——请始终使用but命令,而不是git checkout gitbutler/workspace或在其上直接git commit(后者会把改动提交到合并提交而非你的分支)。

  3. 应用(Applied)与未应用(Unapplied)控制分支的活跃状态

    • 已应用分支:位于工作目录中,改动可见,可以修改并提交;
    • 未应用分支:存在但未激活,不在工作目录中,不能直接改动;
    • 通过but apply/but unapply控制(详见后文"应用与未应用分支"一节)。

这套模型在仓库中有完整的命令实现支撑,见 crates/but/src/command/legacy/ 下的apply.rsunapply.rssetup.rs等模块;工作区层面的实现位于 crates/gitbutler-workspace/。

CLI ID:会话级短标识符系统

GitButler CLI 面向 AI Agent 与脚本自动化设计,因此每个对象都会获得一个短小、可读的 CLI ID,显示在but statusbut diff输出中。这些 ID每次会话动态生成,且在所有实体类型之间保持唯一(不会有两个对象共用同一个 ID)——所以永远从当前命令输出中读取 ID,不要硬编码或凭空编造

各类对象的 ID 形态

对象ID 示例说明
提交 Commits1,ton,mpq#0有 change ID 的提交使用 change ID 短前缀;否则使用 SHA 前缀;#N后缀用于消歧义共享同一 change ID 的提交
分支 Branchesfe,bu,ui分支名的 2–3 字符唯一子串(如feature-xfe);若无唯一子串则回退为自动生成 ID
文件 Filesuvw,qyo由文件路径推导,长度足以保证唯一
Hunkuvw:2e4,uvw:e2c<文件ID>:<hunk ID>;裸but diff展示未提交 hunk
已提交文件mzm:uvw<提交ID>:<文件ID>,显示在but status -fv的每个提交下方
已提交 hunkmzm:uvw:2e4<提交ID>:<文件ID>:<hunk ID>,由but diff <commit-id>展示
栈 Stacksm0,n0自动生成,2–3 个字符

ID 长度约束

当检测到是 Agent 在操作时,缩短的 change ID、文件和 hunk 前缀有最少 3 个字符的限制;而 SHA 前缀和分支/worktree ID 可以更短。这个设计是为了降低 Agent 误读、误用短 ID 的概率。

读取状态输出的正确姿势

  • 每行输出的第一个 token 就是该行的 ID,直接复制即可;
  • 冗长(verbose)提交行会在时间戳后附加一条信息性的(sha …),它在每次 amend 后都会变化——仅供查看,绝不能把它传给命令

ID 的稳定性与失效语义

理解 ID 的生命周期是安全使用 GitButler CLI 的关键:

  • 分支短 ID只在其产生的那个工作区快照内有效;而分支名在无关的工作区变更中保持稳定,因此对分支做变更操作时请始终使用完整分支名。
  • 文件/ hunk ID从当前输出复制后,在普通提交序列中通常保持可用,可以连续引用多个,包括跨链式but commit调用。如果某个 ID 无法解析,重新读一次but diff再继续即可。
  • 提交 ID:有 change ID 的提交用 change ID 前缀,否则用 SHA 前缀。change ID 引用在历史编辑(amendsquashmoveuncommitreword)后依然有效;而SHA 引用和#N后缀引用会失效——过期的 SHA 可能静默解析到错误的提交上,这是最危险的一类坑。
  • 因此,当一次历史编辑链中涉及的每个引用都是 change ID 引用时,可以基于一次status读取连续执行;否则请逐个执行,并在需要时加--status-after获取下一步所需的引用。

ID 的用法:位置参数

ID 是位置参数、空格分隔的。把 ID 作为参数传给命令:

but commit -b <branch-name> -m "message" <file-or-hunk-id> # 把选中变更提交到分支 but amend -t <commit-id> <file-or-hunk-id> <file-or-hunk-id> # 把文件/hunk 修补进提交 but squash <commit-id> -t <commit-id> -m "message" # 压缩提交 but move <commit-id>:<file-id> --above <commit-id> # 重定位已提交文件 but move <commit-id>:<file-id>:<hunk-id> --above <commit-id> # 重定位已提交 hunk

but help cli-ids会详细说明每一种 ID 的完整规则。需要特别提醒:hunk ID 中冒号后面是hunk 的 ID,不是行号范围(uvw:16-40是非法写法);也不要发明--changes/--hunk/--ids之类的标志,更不要把 ID 用逗号分隔(uvw,qyo会被解析成单个 ID 而失败)。

Worktrees 实验特性中的 ID

工作树支持属于实验特性,只有在worktreeManipulation特性标志开启时才可用(相关实现见 crates/but/src/args/worktree.rs 与 crates/but/src/command/worktree/)。在该模式下:

  • 每个活跃 worktree 有自己的 ID,在but status中以"通道(lane)"形式绘制——一个花括号{<branch>}标题(若其HEAD处于 detached 状态则显示 worktree 名),嵌套在其所依托的提交上方(可以嵌套其他 worktree 的提交,通道递归嵌套),或独立显示在栈下方(当它位于工作区之外时);
  • 通道中列出该 worktree 的未提交文件与提交;标题 ID 即 worktree 的名字;
  • <worktree>:@(ID 或名字均可)表示该 worktree 的未提交区域;<worktree>:<path>将文件名限定到该 worktree——@:<path>仍然表示主工作区。一个文件同时在多个 worktree 中脏时会产生歧义,错误信息会提示使用限定形式;
  • worktree 的文件 ID 或<worktree>:@可以作为but commit的变更来源和but amend的来源:变更落到目标上,并清空该 worktree 的未提交区域;
  • 不带目标标志时,worktree 的变更提交到该 worktree 自身分支的顶端;显式指定的目标提交或分支不必属于该 worktree;
  • 一次操作只从一个 worktree 读取——混合了多个 worktree 的选择会被拒绝;
  • worktree 也可以作为目标:but commitbut movebut pick配合-b <worktree-id-or-its-branch-name>--below <worktree-id>会把提交放到该 worktree 所检出的分支顶端(--above会被拒绝,因为那是它的未提交区域);
  • worktree 自身的提交携带普通提交 ID:rewordmovesquashpick都接受它们,重写后 worktree 的分支与 checkout 会随之更新;
  • 对 worktree 提交执行 uncommit 会落到该 worktree 的未提交区域,因此squash -t要用 worktree 的区域 ID(<id>:@)而不是@来命名它;but uncommit会自动推断。

并行分支与堆叠分支

GitButler 的并行栈模型天然支持两种组织方式:并行分支与堆叠分支。

并行分支(独立工作)

but branch new <name>创建:

main ──┬── api-endpoint (independent) └── ui-update (independent)

适用于以下场景:

  • 任务之间互不依赖;
  • 可以独立合并;
  • 代码之间没有共享。

典型例子:新增一个 API 端点与修改按钮样式,两者完全独立,可以并行推进、分别评审合并。完整实操见 examples.md 中的 Example 1。

堆叠分支(依赖工作)

把已有分支堆叠到另一个分支之上but move <child-branch-name> --above <parent-branch-name>

从零创建新的堆叠分支but branch new <name> --above <parent-branch-name>——仅当子分支尚不存在时使用这个命令。

main ── authentication ── user-profile ── settings-page (base) (stacked) (stacked)

适用于以下场景:

  • 功能 B 需要功能 A 的代码;
  • 在前序工作的基础上增量构建;
  • 创建一系列相互关联的变更。

典型例子:用户资料页需要先实现认证功能。

堆叠两个已有分支:如果两个分支都已存在,需要让其中一个依赖另一个,使用顶层move命令:

but move feature/frontend --above feature/backend # 现在 frontend 堆叠在 backend 之上——两者同属一个栈

从栈中拆离一个分支

but move feature/frontend --unstack

依赖跟踪:GitButler 自动跟踪哪些变更依赖哪些提交。一个依赖变更只能被提交到包含它所依赖的提交的那个栈中。注意,堆叠操作必须使用完整分支名(分支短 ID 只属于产生它的那个快照);提交重排则使用提交 ID。

编辑模型:Sources 与 Target

GitButler 的历史编辑(history editing)统一表达为来源(Sources)目标(Target)。Sources 是位置化的 CLI ID;目标通过标志指定。@是一个特殊 ID,表示"未提交区域"。

but squash承载了这个模型的绝大部分语义——它具体做什么取决于你组合的实体类型:

Sources目标(-t操作示例
提交(一个或多个)提交把多个提交压缩到一起but squash mm -t nn -m "…"
分支提交把整个分支压缩进一个提交but squash <branch-name> -t nn -m "…"
提交(一个或多个)分支压缩进分支的最新提交but squash mm -t <branch-name> -m "…"
分支(无)把分支压缩成单个提交but squash <branch-name> -m "…"
未提交文件提交把变更修补进一个提交but squash a1 -t nn
@提交把所有未提交内容修补进提交but squash @ -t nn
提交@取消提交(uncommit)but squash mm -t @
分支@取消提交并删除分支but squash <branch-name> -t @
已提交变更提交把变更移动到另一个提交but squash nn:a -t mm

消息标志规则:当来源是提交或分支时(除非目标是@),它们会组合成一条新消息——所以必须传-m。不传的话,Agent 运行时会保留组合出的消息(各来源消息的拼接),终端模式下会打开编辑器。其余行复用目标的消息,不需要消息标志;而-t @会直接拒绝消息标志。

与 amend 的重叠:表格中两个 amend 行与but amend重叠——推荐使用but amend -t nn a1,它只做这一件事,接受相同的 ID。

其他编辑命令是同一模型上更窄的入口:

  • but amend -t <commit> <changes>—— 把未提交变更修补进已知提交;
  • but uncommit <commits-branches-or-committed-changes>—— 把已提交工作移回未提交状态;分支会被移除,一次调用中的已提交变更必须来自同一个提交;
  • but move <sources> --above|--below|--branch|--unstack—— 重定位提交、已提交变更或分支;这是带位置控制的命令;
  • but discard <changes>—— 丢弃工作而不是重定位它。

这些命令在仓库中的实现分别位于 crates/but/src/command/legacy/ 下的squash.rsamend.rsuncommit.rsmove.rsdiscard.rssplit.rspick.rsreword.rs等模块。

依赖跟踪:防止破坏性状态

GitButler 自动跟踪变更之间的依赖关系。

工作原理

Commit C1: Added function foo() Commit C2: Added function bar() Uncommitted: Call to foo() in new code

上例中,未提交变更依赖C1(因为它调用了foo())。其推论为:

  1. 不能把这个变更提交到不包含 C1 的栈;
  2. 当把它修补进历史时,它应当属于 C1(或 C1 之后的提交);
  3. 如果试图移动这个变更,GitButler 会阻止无效操作。

为什么重要

依赖跟踪从机制上防止你制造损坏状态:

  • 不能把依赖代码移离它的依赖;
  • 不能把变更提交到错误的栈;
  • 确保每个分支保持独立可用。

but commitbut amend会原子性地失败(错误信息形如 "Cannot commit: N changes could not be applied",并点名每个被拒变更依赖的分支与提交),此时没有任何内容被提交、也没有-b分支被创建。若只有一个依赖分支,错误提示会给出精确的恢复命令(but move <your-branch> --above <dependency-branch>but branch new <name> --above <dependency-branch>);若错误只列依赖而没有提示,运行but status -fv查看依赖提交的位置后再决定放置点。

空提交占位符

你可以创建空提交:

but commit --empty --below nn -m "TODO: Add error handling" but commit --empty --above nn -m "TODO: Add error handling"

使用场景

  1. 标记未来的工作:创建空提交作为后续变更的占位;
  2. 组织历史:在提交历史中加入语义标记。

典型工作流:

but commit --empty --below rr -m "TODO: Add error handling" # 之后,把错误处理的变更修补进占位提交 but amend -t <empty-commit-id> <file-id>

这让你可以在历史中"预定"一个位置:先立起占位符,再把后续改动通过amend精确落到它上面,保持历史叙事清晰有序。

操作历史(Oplog)

GitButler 中的每一个操作都会被记录到 oplog(operation log,操作日志)中。

记录内容

  • 分支的创建/删除;
  • 提交;
  • squash / amend / move / uncommit / discard 操作;
  • push / pull 操作。

使用 Oplog

but oplog # 查看历史 but undo # 撤销上一个操作 but redo # 重做上一个已撤销的操作 but oplog list --since <snapshot-id> but oplog list --snapshot but oplog snapshot -m "known good" but oplog restore <snapshot-id> # 恢复到特定时间点

可以把它理解为"git reflog",但记录的是所有 GitButler 操作,而不仅是分支移动。

安全网:犯了错误?but undo撤销它。想回到更早的状态?but oplog restore恢复到之前的快照。

源码层面的实现

oplog 的 CLI 实现位于 crates/but/src/command/legacy/oplog.rs:oplog list支持--since <sha-prefix>过滤,并通过snapshots_iter遍历快照序列(每个快照的commit_id即其短 ID,展示为 git SHA 而非 CLI ID);oplog restore先通过get_snapshot取出目标快照、peel_restore_snapshot处理快照链,再调用restore_snapshot_with_kind恢复工作区(恢复完成后输出 "Workspace has been restored to the selected snapshot.")。底层存储位于 crates/gitbutler-oplog/src/(oplog.rssnapshot.rsentry.rsstate.rsreflog.rs),undo/redo命令实现在 crates/but/src/command/legacy/undo_redo.rs(pub fn undopub fn redo)。

注意:but oplog restore快照引用是 git SHA,不是 CLI ID——从but oplog输出中复制 SHA 使用。由于 oplog 记录所有操作,but undo不仅能撤销提交类操作,还能撤销 squash、move、resolve 等历史编辑操作,恢复被错误合并的状态。

应用与未应用分支

分支存在两种状态:

已应用分支(Applied)

  • 在工作区中处于激活状态;
  • 已合并进gitbutler/workspace
  • 改动在工作目录中可见;
  • 可以修改并提交。

未应用分支(Unapplied)

  • 存在但未激活;
  • 不在工作目录中;
  • 不能直接修改(必须先用 apply);
  • 适合临时搁置工作。

控制状态

but apply <branch-name> # 激活分支 but unapply <branch-name> # 停用分支

使用场景

  • 停用引起冲突的分支;
  • 聚焦工作子集(停用其他分支);
  • 临时搁置工作而不删除。

注意:GitButler没有 stash 概念。当需要"暂存"未提交改动时,正确做法是but commit -b <branch> -m "wip" <ids>提交一个 WIP 提交,之后需要时再but uncommit取回(SKILL.md 中明确要求不要手工还原文件)。另外,标记为(merged upstream)的分支表示已合入上游,运行but pull即可移除它们。

冲突解决模式

but pull引发冲突时,受影响的提交会被标记为冲突状态。冲突不会中断操作——rebase 总会完成,冲突提交在but status中被标记为{conflicted}

解决流程

  1. 识别but pull的摘要会按从旧到新的顺序列出每个冲突提交的 ID(but status也会显示它们);
  2. 进入模式but resolve <commit-id>—— 它会打印带行号的冲突区域。有多个冲突提交时,从最旧的开始解决:完成靠下的提交会自动把其上的提交 rebase 到新基础上;
  3. 修复冲突:编辑文件、移除冲突标记(<<<<<<<|||||||=======>>>>>>>)。多个文件冲突时but resolve status会重新列出剩余项;
  4. 收尾but resolve finishbut resolve cancelfinish会报告剩余标记与幸存的未提交变更,因此无需再额外检查。

解决期间的限制

  • 处于专注于该提交的特殊模式中;
  • 其他 GitButler 操作受限;
  • but status会显示你正处于解决模式;
  • 必须完成(finish)或取消(cancel)后才能继续正常工作。

更精细的做法(Agent 工作流)不进入模式,而是逐冲突应用:but resolve conflicts <branch>列出冲突、but resolve apply <path>:<N>通过 stdin/--file应用合并内容或--ours/--theirs整侧采用、--ai委托给配置的 AI 模型,全程只需but undo即可回退错误的解决——详见 SKILL.md 的"Resolve conflicted commits"小节。解决冲突期间严禁使用git addgit commitgit checkout --theirs/--ours等任何 git 写命令,只需直接编辑文件。

只读 Git 命令的边界

不修改状态的 Git 命令可以安全使用:

安全(只读):

  • git log—— 查看历史;
  • git diff—— 查看变更(但更推荐but diff,它支持 CLI ID);
  • git show—— 查看提交;
  • git blame—— 查看行历史;
  • git reflog—— 查看引用日志。

不要在 GitButler 工作区中使用:

  • git status—— 具有误导性:它显示的是合并后的工作区状态,而非各栈的状态;而且缺少 Agent 需要的 CLI ID;
  • git commit—— 会提交到工作区合并提交,而不是你的分支;
  • git checkout—— 破坏工作区模型;
  • git rebase—— 与 GitButler 的管理相冲突(rebase 应用分支到最新目标就是but pull,绝不是moveconfig target或裸git pull/git rebase);
  • git merge—— 使用but merge代替。

经验法则:能读就行;要写就用but在仓库的 skill 定义中,这条规则被固化为不可妥协的第一原则:所有写操作必须走but,当用户表达一个 git 写命令时,翻译成but再执行。but与 git 写命令的完整映射见 SKILL.md 的 "Git-to-But Map" 表格(如git add+git commitbut commit -b <branch> -m ... <ids>git rebase -ibut move/but squash/but rewordgit cherry-pickbut pickgh pr createbut pr new)。

小结

GitButler CLI 的核心概念可以浓缩为一句话:用并行栈替代串行 checkout,用会话级 CLI ID 替代记忆中的哈希,用 Sources/Target 编辑模型统一所有历史改写,用 Oplog 兜底每一次误操作。理解并遵循"只读 Git 命令可用、写操作一律走but"的边界,就能在享受多分支并行工作流的同时,保持历史清晰、状态可回溯。相关配套文档还包括命令速查 reference.md(可用but skill reference从 CLI 直接渲染)与实战工作流示例 examples.md,以及随时可用的but help cli-idsbut skill concepts

【免费下载链接】gitbutlerThe GitButler version control client, backed by Git, powered by Tauri/Rust/Svelte项目地址: https://gitcode.com/GitHub_Trending/gi/gitbutler

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询