---
url: /blog/fn1bkbfn/index.md
---
原文：https://sspai.com/post/95404

## 「原子化」提交，一个提交只做一件事

小时候，老师和长辈或许都告诫我们，在做事情的时候要「一心一意」，不要分散注意力到其他的事情上，这样做事才会高效、有成果。

一旦没有遵循「一心一意」的原则，那么干正事的时候碰壁就再寻常不过。就拿我本人来说，做正事的时候「摸鱼」，狂欢一阵子后再回来，面对还没有完成的正式任务，我是容易一脸懵的：我做了啥？进度怎么样了？我刚刚是怎么攻关难题的来着？——显然，这导致了时间和精力的浪费，更不利于事后复盘和提升。

不单单是做事，在使用 Git 的过程中，做每一个提交时也需要「一心一意」，也就是「原子化」（atomic）——确保每个提交只完成一项任务。

### 1）引例

可能一些开发者在组织 Git 提交时，习惯让一个提交做多件事情，**把多项各自独立且不相关联的功能修改都放在同一个提交里**。比如：

* 一个项目有 A、B、C 三个功能模块，作者对这三个模块都做了修改，还顺带在 2025 年来临之际修改了所有源码开头注释里版权说明的年份 1。
* 一个算法函数有多个各自独立的部分，作者在一个提交里就对这几个部分做了修改。
* 作者原本在写 A 函数，但是发现 B、C、D 这几个函数的注释需要补充，于是统统做了修改，并放在同一个提交里。

这种提交方式看似省事儿，但无疑会给后续的开发工作「埋雷」。

### 2）为什么会「埋雷」？

根据笔者自己的经验体会，如果一个提交做了不止一件事情，那么有可能会在你日后开发的时候，给你带来一些「麻烦」：

#### 0x00：难以排错

也许在开发过程中，某个提交给项目引入了 Bug。当你好不容易找到了存在问题的提交，但却发现这个提交做了不止一件事，你还要花费额外的精力来定位造成 Bug 的地方。

而如果你有不止一个提交都没有做到「一心一意」，那么你就很难直接浏览提交记录来找到问题所在，你就不得不翻阅一个个提交，浪费成倍的精力。

#### 0x01：不便复盘

很多时候，开发者需要复盘自己的提交记录，看看自己做了哪些工作。然而，**假如一个提交做了不止一件事，那么就不便于复盘开发过程。**

例如，当你想要回顾某个功能的实现过程，找出实现该功能的提交时，却发现关键的代码都被混在几个大的、综合的提交里，难以找出。好比是考前复习的时候，你总结的关键知识点混在了一堆厚厚的复习资料里，自然会影响复习效率。

或许有些读者会选择详尽编写提交说明，通过检索提交说明来找出自己想要的提交。不过这带来的好处有限——你后续还是离不开使用 `git show`，在一长串「包罗万象」的 diff 记录中，找到真正想找的修改记录。

### 2）「原子化」提交

践行「原子化」提交的理念，以上的麻烦将不复存在。

C++ 从 C++11 标准开始，引入了「原子操作」（atomic）特性，可以确保一个变量在同一时间只有一个操作者在读写，从而防止多线程抢占造成冲突。我借鉴了「原子操作」的理念，提出了「原子化」提交——也就是确保每个提交只完成一个任务，从而改进提交记录的组织方式，防止潜在的杂乱和冲突。

原子化提交最大的优势，在于**提交记录简洁、可追溯、易于查错**：

* 使用 `git log` 浏览提交记录时，你的开发进程一目了然，可以精确到每个提交实现的功能、修复的 Bug，等等，十分利于复盘。
* 一旦某个提交出现问题，你可以快速浏览提交记录，短时间内找到可能存在问题的提交。然后，你就可以通过 `git checkout <疑似有问题提交的前一个提交>`，暂时回到应用该提交前的状态，来排查故障。
* 在实现一个新功能、修复一个 Bug 的时候，你可以先浏览提交记录，快速判定之前是否有做了相关工作，从而避免「无用功」。

### 3）实践「原子化」提交

下面，我就以几个在实际开发场景中的例子，来演示「原子化」提交的操作。

#### 实例一：修复 Bug 的「原子化」提交

假设你正在开发一个电商平台的项目，最近有用户反馈在使用优惠券时，无法正确抵扣金额。经过排查，你发现是优惠券计算逻辑中的一个 Bug。你修复了这个 Bug，并且为了提高代码的可读性，对相关代码进行了重构，将一些冗长的函数拆分成了更小的函数。

如果你遵循「原子化」提交的原则，你可以将这个过程分为两个提交：

| 提交序号        | 提交信息（commit message）                                | 提交内容                                                              |
| --------------- | --------------------------------------------------------- | --------------------------------------------------------------------- |
| 1：专门修复 Bug | `Fix discount calculation bug in coupon system`           | 在这个提交中，你只修改了计算优惠券金额的代码，确保 Bug 被修复。       |
| 2：进行代码重构 | `Refactor coupon calculation code for better readability` | 在这个提交中，你对代码进行了拆分和重构，但不涉及任何 Bug 修复的逻辑。 |

这样做的好处是，**如果后续发现修复 Bug 的代码引入了新的问题，你可以很容易地通过** `**git log**` **找到第一个提交，然后使用** `**git checkout**` **检出到修复 Bug 之前的版本**，快速定位问题。同时，代码重构的提交也不会干扰到 Bug 修复的提交，使得提交记录清晰明了。

#### 实例二：新功能开发的「原子化」提交

假设你正在开发一个在线教育平台，需要添加一个新的功能：允许用户上传视频作业。这个功能涉及到多个方面的开发，包括前端页面的设计、后端接口的编写以及数据库表结构的调整。

如果你遵循「原子化」提交的原则，你可以将这个功能的开发过程分为多个提交：

| 提交序号              | 提交信息（commit message）                          | 提交内容                                                                                             |
| --------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| 1：专注于前端页面设计 | `Design video assignment upload page`               | 在这个提交中，你创建了新的 HTML 文件和 CSS 样式，实现了页面的基本布局和样式。                        |
| 2：编写后端接口       | `Implement backend API for video assignment upload` | 在这个提交中，你创建了新的 API，实现了视频文件的接收和存储逻辑。                                     |
| 3：调整数据库表结构   | `Add database table for video assignments`          | 在这个提交中，你创建了新的数据库表，用于存储视频作业的相关信息，如用户 ID、作业 ID、视频文件路径等。 |

通过这样的「原子化」提交，你可以清晰地记录每个功能模块的开发进度，便于后续的复盘和维护。如果某个模块出现问题，你可以快速定位到相应的提交，进行针对性的排查和修复，而不会被其他模块的代码干扰。

#### 实例三：性能优化的原子提交

假设你正在开发一款合成器软音源插件，最近发现插件占用处理器资源过高，影响用户体验。经过分析，你发现是由于合成器的振荡器、滤波器算法优化不当，且编译参数存在问题，拖累性能。为了优化性能，你需要调整合成器算法，并修改编译系统的编译参数。

如果你遵循「原子化」提交的原则，你可以将这个优化过程分为多个提交：

| 提交序号                      | 提交信息（commit message）      | 提交内容                                                                   |
| ----------------------------- | ------------------------------- | -------------------------------------------------------------------------- |
| 1：优化振荡器算法             | `Optimize oscillator with SIMD` | 在这个提交中，你使用 SIMD 指令来改善振荡器的性能。                         |
| 2：优化滤波器算法             | `Optimize filter with SIMD`     | 在这个提交中，你同样使用了 SIMD 指令，来提升滤波器的工作效率。             |
| 3：优化编译系统的编译参数设置 | `Enable compiler optimization`  | 在这个提交中，你为编译系统添加了编译器优化参数，使编译器充分优化代码性能。 |

通过这样的原子化提交，你可以清晰地记录每个优化措施的实施过程，便于后续的评估和维护。如果某个优化措施带来了新的问题，你可以快速定位到相应的提交，进行针对性的排查和修复，而不会被其他优化措施的代码干扰。

### 4）原子化提交的潜在优势

值得一提的是，原子化提交除了有利于你自己的代码管理，还能起到「利他」的作用。

在 GitHub 等开源社区中，原子化提交可以方便其他开发者浏览你的提交记录和开发进程，使你的开源成果更好惠及他人。井井有条的提交记录，还可以给他人留下好印象，彰显你的专业水准。

另一方面，像 Linux 这样的大型项目，每个提交也都是原子化的，并且更加规范，这大大方便了二次开发时对新特性的反向移植工作。

我自己也在做 Android 内核的适配，曾经将新版本内核的特性反向移植到华为 P6 的 3.0.6 内核中。开发过程中，我会从新版本内核的提交日志中找到与某个特性有关的补丁，然后将其应用到当前内核中，几乎是一找一个准，节省了在海量代码中「找重点」的精力。

可见，原子化提交利己利人，具有非常显著的潜在优势，受益不只一点点。

## 善用 `git rebase`，合并细碎提交

记得大概是 2018 年，我在刷知乎的时候见到一个提问：[《Git commits 历史是如何做到如此清爽的？》](https://sspai.com/link?target=https%3A%2F%2Fwww.zhihu.com%2Fquestion%2F61283395)，提问者非常惊讶于知名前端框架 Vue.js 源码库提交记录的干净、清爽。

!\[image-20250726200720541]\(./Git 原子性提交.assets/image-20250726200720541.png)

> [Vue.js core 组件提交记录](https://sspai.com/link?target=https%3A%2F%2Fgithub.com%2Fvuejs%2Fcore%2Fcommits%2Fmain%2F)的一部分。

就在这个问题下，作者尤雨溪（Evan You）\[现身说法]\(https://sspai.com/post/Git commits 历史是如何做到如此清爽的？ - 尤雨溪的回答 - 知乎 https://www.zhihu.com/question/61283395/answer/186122300)：\*\*多用 rebase。\*\*

就是大佬这短短三个字的经验谈，让我对 git rebase 这个功能产生了浓厚的兴趣。那时的我恐怕也不会想到，后来的我会如此频繁地使用 rebase，从而摆脱既往使用 Git 时提交记录琐碎、杂乱的问题，离正确使用 Git 更近一步。

### 1）琐碎的提交从何而来？

大家都知道，作为版本管理系统，Git 要保证提交记录可靠、可追溯，因此不能像文本编辑那样随意更改提交记录，最多只提供 `git commit --amend` 功能来允许你更改最新的提交。

但在开发过程中，或许你不太可能时时刻刻像 Vue.js 的提交记录那样，使自己每个提交都保持清爽、规范。回头使用 git log 浏览提交记录，你可能会发现你提交了太多琐碎的内容，「细致」到写一行代码、修一个错别字、修一个注释都有单独的提交。

很多时候这些提交是很琐碎的，没有必要单独保留，好比是你在文字处理软件中每写一句话就按“保存”，而每个修改都被单独保存成了一个文件。久而久之，提交记录就变得冗长琐碎，管理的时候就很难追溯到有用的、重要的提交。

这个时候， `git rebase` 就派上用场了。

Git 的设计师考虑到用户整理提交记录的需求，于是就设计了 `git rebase` 这一功能，允许你合并、编辑、重排已有的提交，使修改后的提交井然有序，就像 Vue.js 的提交记录那样清晰。

### 2）实战演示如何合并琐碎提交

以下面这个提交记录为例，记录了某项目从零开始写 `main()` 函数的过程，仅仅是添加文本输出与修改注释的提交就有好几个。

> \*\*注意：\*\*假设这些提交彼此之间没有冲突，每个提交都是在原有提交之上的微调。

```cmd
$ git log --pretty=oneline    # 使用单行模式输出提交记录。最新的提交在前。
3a51f37493191451413b8dc7428d63351ce4b1e3 (HEAD -> main, origin/main) main: 修改注释中的错别字
6587eeb437c8b139965085ddf99bd72bae682f89 main: 添加注释
61b04c318c24434996587eeb437c8b13996587e6 main: 添加操作结束的文本输出
61500445ebae1eb855ab216c6bbcec6ee73bd270 main: 微调操作开始的文本
7dad6bec684949ab0188085ddf99bc724c7b7b59 main: 添加操作开始的文本输出
996587eeb437c8b13996451413b8dc7603452f32 main: 添加基本文本输出
554bb6603452f3bf5705ac200effbdfc0aa97465 main: 创建main函数
```

接下来，笔者就要把所有提交都合并到第一个提交「`main: 创建main函数`」\*（以下简称「目标提交」）\*当中。

#### 0x00：检查代码树是否有未提交的更改

为了防止 rebase 弄乱代码仓库，Git 强制要求你的工作区「干净」，也就是不存在已经修改但没有提交的文件。否则，你是没有办法 rebase 的。

根据你的开发进度，你可以先提交修改；或者是使用 `git stash` 将修改暂时保存起来，等到完成 rebase 后再运行 `git stash pop` 恢复你的修改。

#### 0x01：进入 git rebase 模式

使用 `git log` 查看提交记录，记住「目标提交」的 ID。然后，运行以下命令，开始 rebase：

```
git rebase -i 554bb6603452f3bf5705ac200effbdfc0aa97465~
```

这个命令，允许你修改从「最新提交」到「目标提交」在内的所有提交。**注意不要漏了提交 ID 后面的波浪线，否则会把「目标提交」给漏掉**2\*\*。\*\*

稍等片刻，Git 会自动打开文本编辑器（通常是 Vim 或 Nano），列出一系列提交。其中：

* **较新的提交列在文档的后面**3\*\*，顺序是与\*\* `**git log**` **相反的；**
* 每一行前面的 `pick` 是操作指令，意为采用该提交。这是默认的行为。

```cmd
pick 554bb660 main: 创建main函数
pick 996587ee main: 添加基本文本输出
pick 7dad6bec main: 添加操作开始的文本输出
pick 61500445 main: 微调操作开始的文本
pick 61b04c31 main: 添加操作结束的文本输出
pick 6587eeb4 main: 添加注释
pick 3a51f374 main: 修改注释中的错别字
```

#### 0x02：合并提交

在本例中，从`996587ee` 到`3a51f374` 的这几个提交，都是要并入「目标提交」的提交。

我们把这些提交对应行行首的 `pick` 改为 `fixup` （或单个字母“`f`”）。`fixup` 指令的作用是合并提交，但是只保留前一个提交（相邻一个比它早的提交）的说明。如下所示：

```cmd
pick 554bb660 main: 创建main函数
f 996587ee main: 添加基本文本输出
f 7dad6bec main: 添加操作开始的文本输出
f 61500445 main: 微调操作开始的文本
f 61b04c31 main: 添加操作结束的文本输出
f 6587eeb4 main: 添加注释
f 3a51f374 main: 修改注释中的错别字
```

随后，保存文件并关闭编辑器，Git 就会自动开始 rebase，一个个把新的提交并入相邻的前一个提交中，直到目标提交。这样，那些琐碎的提交都被并入我们的目标提交中，如此一来提交记录就清爽了不少：

```cmd
$ git log --pretty=oneline    # 使用单行模式输出提交记录
f5815166356e85a5fe244f6024c2e401f04b10fa (HEAD -> main, origin/main) main: 创建main函数
```

**如果你希望保留相关提交的说明文本（以备参考等），那么你可以使用** `**squash**` **指令（或单个字母“**`**s**`**”）**，保存文件并关闭编辑器后， Git 会打开一个新文档，在这里你可以检查、修改提交说明。

> \[!CAUTION]
>
> \*\*注意：\*\*经过 rebase 之后，原本的「目标提交」ID 会发生变化，因为 Git 实际上生成了一个新的提交。

### 3）如果琐碎的提交是后来才做出的

随着项目开发的推进，你达成的目标越来越多，提交数量也随之增长。但你发现一个早期编写的功能里，注释、代码缩进这些细节存在问题，于是再做了几个提交来修改。

现在，你想把这些琐碎的修改合并到该功能的提交中。此时你依然可以运用 `git rebase`，先调整提交顺序，然后再使用 `fixup`（或 `squash`）指令来合并提交。

#### 0x00：准备工作

在继续操作之前，你需要确保仓库里没有未提交的更改。

另一方面，你还要保证那些琐碎的提交不与你既往的修改产生冲突。比如说，如果你的提交除了修改注释，还顺带修改了函数结构、变量定义等内容，那么很可能会与你的其他提交造成冲突，需要你手动干预，造成不必要的麻烦。

> \[!CAUTION]
>
> \*\*注意：\*\*假设下文的提交彼此之间没有冲突，每个提交都是在原有提交之上的调整。

#### 0x01：进入 rebase 模式，重排提交顺序

在运行 `git rebase` 之前，你需要通过 `git log` 检索提交日志，找到你的「目标提交」。

在下面的例子中，目标提交是`554bb660` （「创建 main」函数），你的任务就是要把你的目标是把 `3a51f374` 与 `6587eeb4` 这两个琐碎的提交合并到 `554bb660` 这个提交中。

> 这里是一个简化版的例子。实际的项目当中，你的「目标提交」与待合并的琐碎提交之间可能相隔十几个甚至几十个提交。

```cmd
$ git log --pretty=oneline    # 使用单行模式输出提交记录
...
3a51f374 main: 修改注释中的错别字
6587eeb4 main: 修正代码缩进
996587ee process: 使用libfftw3，优化合成器算法逻辑
34e87ac3 process: 创建process函数
554bb660 main: 创建main函数
...
```

然后，运行 `git rebase` 命令：

```cmd
git rebase -i 554bb660~
```

此时， Git 依然会打开一个文本编辑器，内容如下：

```cmd
pick 554bb660 main: 创建main函数
pick 34e87ac3 process: 创建process函数
pick 996587ee process: 使用libfftw3，优化合成器算法逻辑
pick 6587eeb4 main: 修正代码缩进
pick 3a51f374 main: 修改注释中的错别字
```

我们在 `git rebase` 给我们打开的文本编辑器里，把`3a51f374` 与 `6587eeb4` 这两个提交对应的行，**整体复制到** `**554bb660**` **的后面，并将原有的行注释掉**。就像下面这样：

```cmd
pick 554bb660 main: 创建main函数

# 将提交所对应的行复制到我们的目标提交后面
pick 6587eeb4 main: 修正代码缩进
pick 3a51f374 main: 修改注释中的错别字

pick 34e87ac3 process: 创建process函数
pick 996587ee process: 使用libfftw3，优化合成器算法逻辑

# 为保险起见，将原有的行注释掉，而不是直接移动
#pick 6587eeb4 main: 修正代码缩进
#pick 3a51f374 main: 修改注释中的错别字
```

> \[!CAUTION]
>
> 警告：不要修改 pick 后面的内容，尤其是提交 ID，否则提交记录可能会发生混乱。

#### 0x02：合并提交

确认提交顺序无误后，将待合并的提交对应行行首的 `pick` 指令改为 `fixup` （或 `squash`），如下所示

```cmd
pick 554bb660 main: 创建main函数

# 将提交所对应的行复制到我们的目标提交后面
fixup 6587eeb4 main: 修正代码缩进
fixup 3a51f374 main: 修改注释中的错别字

pick 34e87ac3 process: 创建process函数
pick 996587ee process: 使用libfftw3，优化合成器算法逻辑

# 为保险起见，将原有的行注释掉，而不是直接移动
#pick 6587eeb4 main: 修正代码缩进
#pick 3a51f374 main: 修改注释中的错别字
```

保存文件后，Git 随即开始 rebase 工作，这样我们就可以化琐碎为清爽，得到一个干净的提交记录了。

### 3）注意事项

\*\*在进行 rebase 前，务必要检查你要合并的提交与「目标提交」之间是否存在冲突。\*\*一旦存在冲突，那么 `git rebase` 就无法继续，会要求你手工修改你的仓库代码来处理冲突，这需要更多的时间和精力——因为你要保证代码正常无误。

另一方面，经过 rebase 合并所得的提交，本质上是一个全新的提交，并且也改变了原有的提交记录，因此如果别人 fork 了你的代码，在与你的仓库同步时必定会发生冲突。你或许需要告知你的团队成员，或者是通过 README 来告知代码共享平台 5 上的用户，告诉他们使用 `git pull --rebase` 来同步你的修改。

## 提交顺序不满意？也可以用 rebase 搞定

我自己在找到开发项目的新灵感后，会马上新建一个 Git 仓库，开始动手实践，并把我写的源代码提交到仓库里。然而当我想进一步把仓库上传到 GitHub 时，却发现：我忘了加上许可协议、README 和 `.gitignore`！

考虑到这些文件都是在新建项目时就要添加的，如果我在完成了一部分程序功能后补上去，再回看 Git 提交记录，总是会觉得格外「别扭」。在这样的情况下，**我依然可以运用 git rebase，单独调整提交顺序，还我自己一个科学有序的提交记录。**

### 1）实践如何调整提交顺序

这里举一个高度简化的例子：假设笔者有一个项目，已经完成了程序的主体开发工作，后来才补上 README 与许可协议。提交日志如下（较新的提交在前面）：

```cmd
$ git log --pretty=oneline    # 使用单行模式输出提交记录
0fb4a3b0 添加 README.md
acbc6080 添加许可协议（GPLv3）
a841dbc1 UI 的 bug 修复
16f688a6 DSP 性能优化
75ca23f6 完成 UI 开发
277aad5f 完成 DSP 开发
eb323b0e 初始提交（Initial commit）
```

我希望把与 README、许可协议相关的提交 ── 也就是 `0fb4a3b0` 、`acbc6080` ── 挪到时间顺序上的初始提交之后，也就是提交日志中初始提交的前一行。

#### 0x01：打开 git rebase

在本例中，我已经确定好了「目标提交」，也就是初始提交，并且已经确保工作区没有未提交的代码。

然后，运行 git rebase，定位到目标提交：

```
git rebase -i eb323b0e
```

接下来 Git 会打开文本编辑器，显示以下内容（较新的提交在文档的后面）：

```cmd
pick 277aad5f 完成 DSP 开发
pick 75ca23f6 完成 UI 开发
pick 16f688a6 DSP 性能优化
pick a841dbc1 UI 的 bug 修复
pick acbc6080 添加许可协议（GPLv3）
pick 0fb4a3b0 添加 README.md
```

需要注意的是，git rebase 不能显示初始提交，所以我们能看到的最早的提交是紧邻初始提交之后（比初始提交新）的第一个提交。

### 0x02：开始挪动

先把`0fb4a3b0` 、`acbc6080` 这两个提交对应的行整体复制到 `277aad5f` （也就是初始提交后的第一个提交）前面，然后将原有的那两行注释掉，如下所示：

```cmd
# 将提交所对应的行复制到我们的目标提交后面。
# 本例中的目标提交是初始提交，所以我们实际上是把要挪动的提交放在 git rebase 文档的最前面
pick 0fb4a3b0 添加 README.md
pick acbc6080 添加许可协议（GPLv3）

pick 277aad5f 完成 DSP 开发
pick 75ca23f6 完成 UI 开发
pick 16f688a6 DSP 性能优化
pick a841dbc1 UI 的 bug 修复

# 为保险起见，将原有的行注释掉，而不是直接移动
#pick acbc6080 添加许可协议（GPLv3）
#pick 0fb4a3b0 添加 README.md
```

确认无误后，直接保存，此时 Git 就会自动开始 rebase 工作，稍等片刻再查看提交记录，你会发现提交顺序变了，README 与许可协议所对应的提交就出现在了初始提交的后面，大功告成。

### 2）注意事项

考虑到 rebase 时可能会面临的冲突，你需要三思而后行。

通常只建议重排那些对其他提交记录几乎没什么干扰的提交，例如你的提交只创建、修改了某一个特定的文件（README、许可协议这样的文件）。若重排不慎，你就不得不花费大量的精力来处理 rebase 的冲突，还有可能把提交记录弄乱。6

同样地，你还需要告知你的团队成员或用户，使用 `git pull --rebase` 来合并你的更改。

## 对于未成型项目，`git commit --amend` 也许更适合你

git commit 是 Git 用于提交修改的命令。它有一个参数 `--amend`，允许你修改最新一次提交的内容。有时你发现刚刚提交的代码有错误，或者是提交说明有问题，但是你不想再新建一个提交来修正这些错误，那么你就可以使用 `git commit --amend` 这个命令。

然而，对我来说，`git commit --amend` 的用途远不止于此。**在项目还未成型的阶段，我使用它来保持提交记录的清爽。**

### 1）为什么我会常用 `git commit --amend`？

我开发的项目，主要是将现有的开源音频插件移植到 [DPF](https://sspai.com/link?target=https%3A%2F%2Fgithub.com%2FDISTRHO%2FDPF) 这个跨平台框架 7，这往往是「摸着石头过河」——无论是 DPF 还是被移植的插件，都缺乏文档，全靠我自己摸索。

在早期阶段，代码文件结构、模块和功能代码、编译系统等都还没有定稿，程序也只实现了部分功能：这就是未成型的状态。我个人习惯一边写功能一边调试，常常反复调整代码结构和算法，直到真正实现我预期的目标为止。

\*\*这，往往意味着我要持续修改源代码库。\*\*如此背景之下，如果每个修改都单独提交到仓库里，那么就意味着仓库里会有数十甚至上百条极其琐碎的提交记录，待到项目成型时还要用 `git rebase` 来整理提交。你可以想象一下，用 WPS 写文章，每写一句话就另存一个文件，是什么样的感觉。

### 2）如何妙用 `git commit --amend`？

为了解决上述问题，`git commit --amend` 就成为了我最常用的操作之一。我会采用这样的思路：

* \*\*第一步，\*\*用一个提交来存放项目成型前的所有开发工作。提交说明为「`Early development (WIP!)`」，其中「`WIP（Work In Progress）`」表示该提交随时可能被覆盖。做出这个提交之后，就不要再做新的提交。
* \*\*第二步，\*\*每次完成一部分功能，确认代码编写与程序运行无误时，就使用 `git add` 命令来将修改过的文件暂存（stage）起来，随后使用 `git commit --amend` 更新这个提交的内容。
* \*\*第三步，\*\*继续完成开发，直到项目成型。此时，再运行 `git commit --amend`，重新修改提交说明，将「`WIP`」等字样删除，代表我的早期开发工作已经定稿。

我个人偏向在项目成型之后，才开始转变为以一个个单独提交的方式来持续开发。这里的「成型」，大致可理解为代码结构稳定，程序预期功能已经实现，至少有可以跑起来的 Alpha 版本推出。

如此一来，提交记录就会变得清爽，不会让琐碎的早期开发记录「挤占」你的 git log，事后也不需要再单独花时间来 rebase。

### 3）注意事项

* 由于这种方法并不会为每个更改留下单独的提交，后续难以通过 `git log` 回溯，故只建议在项目未成型时使用。如果你不放心，依然可以每做一组工作就提交一次，对此我建议在提交说明里做标记，以备项目成形后再 rebase 提交记录。
* 在 `git commit --amend` 之前，也务必确保程序与代码无误，以免混乱。

除此之外，当你在为你的项目添加新功能时，你也可以在原型设计、测试的阶段使用`git commit --amend` ，因为这个过程常常就像写一篇新文章，你或许也不希望每写一句话就又来另存一个新文档。

## 写在最后

在日常与 Git 打交道的过程中，我一直致力于精进 Git 的使用。上面这几点技巧，就是我自己的实践成果，着力于让提交日志更清爽、规范，方便后续的维护。让我们再来回顾一下：

* 使用「原子化」提交，每个提交只做一件事，利于管理、维护和回顾你的开发进度；
* 灵活运用 `git rebase`，合并细碎的提交，按需要重排提交，使提交记录清爽、规范；
* 项目未成型时，活用 `git commit --amend`，保持提交记录清爽。

当然，以上的技巧，更多体现出我个人的使用习惯，客观上也改善了我自己 Git 仓库的质量。相信我的分享能为感兴趣的读者朋友们提供参考，一同将 Git 用得更自在。

## 评论区建议

少数派 37194152：

```
rebase只需要注意：
1. 操作必须在本地进行，否则会产生代码冲突，远程协作的成员会生气😠的
2. 不要频繁跟踪、修改大型文件，除非及时清理引用
注意好这些，rebase功能还是很强大的，强迫症极其舒适。
另外，对于频繁使用的指令，应当设置合适的别名，比如git commit --amend我这是gc!（我用的Shell别名）
```

onosense：

```
对于amend命令，我稍微补充一点，如果你之前已经Push到远程仓库，amend后再次推送会导致本地与远程仓库的提交记录不一致，推送失败，这也是新手容易犯的错误。
```
