以柔克刚:化解 Git 操作的疲劳感
Defeating Git Rigour Fatigue with Jujutsu

原始链接: https://ikesau.co/blog/defeating-git-rigour-fatigue-with-jujutsu/

为大型功能特性维护干净的提交历史(即每个提交都是逻辑清晰、便于审阅的步骤)往往令人精疲力竭。像 `jj absorb` 或 `jj squash -i` 这样的传统工具容易出错,或者在边界复杂时导致合并冲突。 作者提出了一种替代工作流:“衣物分类法”(Laundry Sorting)。在开发过程中,与其费力维护完美的提交,不如先让初始提交变成一堆混乱的混合更改。待功能完成后,你先创建一个由空提交组成的链条,按逻辑顺序排列,代表你理想中的历史记录。接着,将所有“混乱”的提交合并为一个“全量”提交,再以交互方式将特定的代码块迁移到对应的逻辑提交中。 这种方法避免了“拆分与合并”的循环,简化了整理过程,并通过一次性分发更改避免了冲突地狱。虽然这有时会产生无法编译的中间提交(牺牲了完美的 `git bisect` 历史),但它显著减轻了版本控制带来的认知负担,将审阅者的可读性和开发者的顺畅度置于严格的增量提交规范之上。

这篇 Hacker News 的讨论探讨了关于 **Jujutsu (jj)** 的争论。这是一个构建在 Git 后端之上的版本控制系统,旨在减少人们在使用 Git 时常感到的“严苛疲劳”。 **支持者**认为,Jujutsu 将提交视为可变对象,并支持更简便的历史记录操作(如持续的变基和冲突管理),消除了 `git rebase` 中常见的模式错误,从而显著降低了使用门槛。他们强调了 `jj undo` 等功能,以及在无需命名的情况下处理多个“匿名”分支的能力。对于用户而言,它提供了一种更符合人体工程学的体验,本质上“消除了”传统 Git 的痛苦。 **怀疑论者**则强调,Git 本身已经是功能强大的行业标准工具。许多人认为,对于那些已经精通 Git 的用户来说,Jujutsu 的优势微乎其微;他们指出,复杂的工作流程可以通过别名、AI 代理或 Magit 等工具来处理。批评者还对 Jujutsu 的分支管理表达了不满,特别是偶尔需要手动推进书签(bookmarks)带来的额外工作量;并指出它面临“Blub 悖论”:如果不投入专门的时间去学习,就很难理解它的优势,而许多资深开发人员并不愿意进行这种投入。
相关文章

原文
This post assumes a basic level of familiarity with the jujutsu version control system. If you haven't used jujutsu, you'll still get the gist of the idea, but I recommend reading Steve's Jujutsu tutorial after.

When developing a large feature, writing Good Commits is hard.

And by Good Commits, I mean something like:

define types add DB functions server CRUD client API client UI

This allows reviewers to step through your pull request in small bites, with each set of changes scoped to a single aspect of the feature.

So, naturally, here's what I do instead:

define types add DB functions WIP test code server CRUD client API and UI fix DB function fix UI bug refactor CRUD fix another UI bug

Latter commits overwrite work that was done in earlier commits and the story breaks.⚖️

Jujutsu makes it easier to hop around commits and iterate quickly on compartmentalized changesets, but it's still effortful and I get averse.🤖

jj absorb helps somewhat, as does jj squash -i, but they both have their downsides:

  1. absorb assigns the changes based on whichever previous commit most recently touched those files, which sometimes doesn't actually correspond to which commit should own these particular changes.
  2. squash can get you stuck in merge conflict hell if your boundaries aren't extremely clean.

So here's a solution to this problem of "git rigour fatigue" that I've come up with.

For this example, let's represent commits visually. Imagine red represents changes to the type definitions, blue to the UI and so on:

Mayhem. Our first commit is a mix of red and blue. We touch red in multiple places!

To fix this, let's create our ideal commit history first, using jj new -B messy-first -m 'red'

Then we can do the rest. (I switch to jj new -A red -m 'blue' at this point)

Then we squash all the commits with actual changes in them into one with jj squash --from messy-first..messy-last --into messy-first

Then we use jj squash -i --from --into red and pick out the red changes, putting them into the red box:

And so on:

Eventually everything's in the right place and the "everything commit" is empty.

For large features, I find this workflow far easier than having to maintain strict git rigour for the lifecycle of the feature's development. It's easier to make improvised commits with temp debugging state in them and tidy it all up in one sweep at the end.

preemptions:

  1. I don't have a good name for this technique. "Doing Commits Like A Big Pile Of Laundry", perhaps?
  2. This is different from (and, imo, superior to) jj split:
    1. With split, if I miss a hunk that should have been in red, I have to split again and squash.
    2. This technique more easily allows sorting the easiest hunks at the beginning without worrying about how it will effect the commit sequencing.
  3. This reason why doing it all at the end is (often) better than using jj squash -i as you go is because the final state of the everything commit is guaranteed to not have any conflicts. Creating a new "fix red and green" commit and interactively squashing that into your red and green commits might break your blue commit if it happens to touch one of the affected files.
  4. A downside to this technique is that there's no guarantee that every commit will compile, which might be a dealbreaker.

⚖️ Some people prefer this, as it helps git bisect work better. Debuggability versus reviewer convenience is the tradeoff, I guess.

🤖 Especially in a world of LLM agents, that will gladly fix bugs for you that span multiple boundaries in 30 seconds.

联系我们 contact @ memedata.com