Git 交互式补丁模式的全面介绍

2025-06-09

Git 交互式补丁模式的全面介绍

当你正在努力完成一个项目时,你意识到:“我已经一个小时没有提交了。”或者,更糟糕的是:“我未暂存的更改代表了多个工作单元”——无论这些单元是功能、重构中的步骤还是错误修复。

假设你运行程序git status后,看到多个文件中的未暂存更改。进一步假设,你可以确信这些更改在各个文件中的划分方式与离散工作单元的划分方式相对应。到目前为止,一切顺利;你已经能够分别对文件git addgit commit文件进行操作,从而生成一个历史记录,其中各个工作单元彼此之间进行了适当的分隔。

但是,如果单个文件中存在代表不同工作单元的更改,该怎么办?假​​设,在你毫无章法地修改代码时,你为中的一个app.js函数添加了新功能,同时又为同一个文件中的另一个函数添加了错误修复?

此时,git add单独创建文件对您来说毫无帮助;您需要能够暂存和提交文件内的区域。在 Git 中,这些区域被称为“块”。为了使用“块”,Git 提供了一个交互式补丁模式,我们可以使用命令git add -patch或进入该模式git add -p

补丁模式介绍

补丁模式是一个小型的 CLI 应用程序,它可以智能地将未暂存的更改划分成多个块,并依次呈现给我们,以便我们决定是否暂存每个块。(它可以直接通过 访问git add -p,也可以在git add -interactive界面中使用,本文不再赘述。)

假设我们从app.js如下所示的内容开始:

const countToNumber = number => {
  console.log(`Let's count to ${number}`);
  for (let i = 0; i < number; i += 1) {
    console.log(i);
  }
  console.log(`We counted to ${number}!`);
};

const numbers = [100, 1000, 50, 35];

numbers.forEach(number => {
  countToNumber(number);
});
Enter fullscreen mode Exit fullscreen mode

假设我们做了一些修改。在意识到countToNumber距离目标还差一步之遥后,我们在初始化循环的那一行<改为。<=

然后我们决定不在countToNumber这个文件中调用 ,而是导出一个可能产生副作用的函数到控制台,以便在其他地方使用。我们将最后三行代码包装在一个函数中,并导出它:

export const processNumbers = numbers => {
  numbers.forEach(number => {
    countToNumber(number);
  });
};

Enter fullscreen mode Exit fullscreen mode

numbers现在我们准备导出我们编写的函数,我们还删除了(可能)用来测试我们工作的数组。

最后,我们决定还要公开一个在打印到控制台之前过滤数字的函数,因此我们添加了一个过滤函数:

export const filterNumbers = (numbers, maximum) => {
  return numbers.filter(n => n < maximum);
};
Enter fullscreen mode Exit fullscreen mode

完成这些更改后,我们意识到需要将它们拆分。因此,我们运行git add -p app.js,git 返回以下内容:

diff --git a/app.js b/app.js
index eceb575..c97e738 100644
--- a/app.js
+++ b/app.js
@@ -1,13 +1,17 @@
 const countToNumber = number => {
   console.log(`Let's count to ${number}`);
-  for (let i = 0; i < number; i += 1) {
+  for (let i = 0; i <= number; i += 1) {
     console.log(i);
   }
   console.log(`We counted to ${number}!`);
 };

-const numbers = [100, 1000, 50, 35];
+export const processNumbers = numbers => {
+  numbers.forEach(number => {
+    countToNumber(number);
+  });
+};

-numbers.forEach(number => {
-  countToNumber(number);
-});
+export const filterNumbers = (numbers, maximum) => {
+  return numbers.filter(n => n < maximum);
+};
Stage this hunk [y,n,q,a,d,s,e,?]?
Enter fullscreen mode Exit fullscreen mode

注意:不要过度解读diff --git输出中的这一行;该diff命令没有这个选项。这只是输出中的git diff一个硬编码字符串,用来帮助我们理解 git 正在做什么(参考我们已知的 知识diff),同时也指示此 diff 输出的格式(统一 diff 格式的变体)。如果你使用过git diff,那么你对这里看到的内容应该很熟悉。

git diff顺便说一下,的输出可以重定向到一个名为patch的文件中,该文件可以保存在某处或传递给朋友,然后再应用:git diff > mypatch.patch然后git apply mypatch.patch。 (或者,如果您想对暂存而不是未暂存的更改执行此操作,请重定向git diff --cached。)这与我们在此处探索的工具被称为“补丁模式”有关,但关于 git 在这方面如何工作的完整探索值得单独写一篇文章。

我们可以看到 git 已决定将所有更改组合成一个块。但我们希望将它们拆分开来。稍后我们将讨论所有命令;现在我们选择s拆分这个块,git 只会显示我们想要的部分:

@@ -1,8 +1,8 @@
 const countToNumber = number => {
   console.log(`Let's count to ${number}`);
-  for (let i = 0; i < number; i += 1) {
+  for (let i = 0; i <= number; i += 1) {
     console.log(i);
   }
   console.log(`We counted to ${number}!`);
 };
Enter fullscreen mode Exit fullscreen mode

现在我们开始讨论。让我们选择y,暂存这块代码,然后q,执行“[q]uit”。这会让我们退出交互模式并返回到命令行。如果我们运行git status,我们会看到 中同时存在已暂存和未暂存的更改app.js——这正是我们想要的。我们可以使用类似git commit -m "fix bug which stopped count just short of number",提交第一个工作单元,然后暂存下一个单元。

因此,我们运行git add -p app.js。使用 拆分后s,git 将更改隔离到processNumbers

@@ -6,5 +6,9 @@
-const numbers = [100, 1000, 50, 35];
+export const processNumbers = numbers => {
+  numbers.forEach(number => {
+    countToNumber(number);
+  });
+};
Enter fullscreen mode Exit fullscreen mode

现在我们像以前一样选择yq然后我们可以进行提交:git commit -m "export function which wraps countToNumber"。我们可以回到交互模式,但我们知道唯一剩余的未提交的更改代表单个工作单元,因此我们可以添加它并以通常的方式提交。

现在我们有了历史记录,其中我们的提交代表了离散的工作单元:

commit bc80191fe63bf75bae7d976b61cf1c24e9391097 (HEAD -> master)
Author: Dev.to Reader <dev.to@reader.com>
Date:   Sun Jul 28 10:48:37 2019 -0500

    export function for filtering lists of numbers

commit 6c36b351cc2a8cc0f6f3582b3221f5e427980fbc
Author: Dev.to Reader <dev.to@reader.com>
Date:   Sun Jul 28 10:47:02 2019 -0500

    export function which wraps countToNumber

commit 0b59f6917215a5c264735e4ed01cf6d1f3c977e4
Author: Dev.to Reader <dev.to@reader.com>
Date:   Sun Jul 28 10:40:25 2019 -0500

    fix bug which stopped count just short of number
Enter fullscreen mode Exit fullscreen mode

修补模式命令

运行?h将显示修补模式下可用的命令:

y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
e - manually edit the current hunk
? - print help
Enter fullscreen mode Exit fullscreen mode

我们已经了解了yqs,它们分别允许我们暂存多个块、退出以及拆分成更小的块。我们很快会回到拆分的话题。

很常见的是,在进入补丁模式并进行拆分后,我会翻阅jJkK文件的各个部分来确定自己的位置,在暂存一些内容并退出之前,寻找可以组合在一起的更改模式。

git add -p app.js重要的是,补丁模式可以在文件( )或整个存储库( )上调用git add -p,因此如果您对多个文件进行了更改,但知道只想在一个文件中进行批量处理更改,则可以这样做。ad旨在用于我们在路径规范中有许多文件的情况下进入补丁模式的情况,因为它们可以帮助我们批量处理文件中的更改。通常,如果我们想根据更改出现的文件批量处理更改,我们可能不会处于交互模式;因此我发现自己d有时会使用,但a几乎从不使用。

g命令会给我们一个交互式菜单,显示所有文件的文件名和行号。我发现自己不怎么用这个,因为在它们呈现给我之前,我通常不知道自己想要暂存哪些文件和哪些行。

然而,这个/命令非常有用。当我处理大量修改,并发现一些工作需要归类,并共享一些通用的可搜索功能(例如,对特定变量或函数的引用)时,我会使用/和来缩减修改,y直到/不再返回结果。

选择后,e当前区块会在你配置 git 使用的编辑器中打开(git config --global core.editor),以便逐行暂存。当你达到交互模式允许的最小区块大小,并且需要对提交的内容进行非常精细的控制时,此功能非常有用。我们稍后会再讨论这一点。

添加意向

如果你向仓库添加一个新文件,添加一些内容,然后运行git diff​​,你将不会在输出中看到任何新文件的内容。这是因为git diff显示了相对于暂存区域的未暂存更改,而如果我们尚未跟踪某个文件,则该文件尚无法进行比较。

由于交互式添加模式是基于 构建的git diff,因此对新的未跟踪文件的更改最初在补丁模式下是不可见的。我们当然可以修改git add该文件,但这会导致一个难题,因为现在我们已经将整个文件暂存了。有几种方法可以解决这个问题。

一个不太常规的解决方案可能是使用git reset -p-- 因为它git reset也有一个交互式补丁模式。我们可以使用它来取消暂存新文件中的所有更改(除了我们打算提交的更改),然后进行提交。

但这很麻烦。更常规、更快捷的解决方案是使用 命令git add --intent-to-add(或其简写git add -N)。这允许我们将文件(但不包含其内容)添加到暂存区,这意味着git diffgit add -p能得到我们想要的结果。

开发人员工作流程和 Hunks 的限制

偶尔需要e会暴露交互式补丁模式的一些局限性。选项git diff允许我们为块指定一些默认值:git config --global diff.context允许我们设置围绕更改提供的上下文行数,而diff.interHunkContext选项允许我们设置块之间应显示的行数,从而允许我们对块的大小进行一些控制。

但是,尽管我们可以控制默认值,git总是将相邻行的更改分组在一起。如果连续行上的更改代表不同的工作单元,我们需要使用 手动编辑补丁e,这使我们能够对提交的内容进行最细粒度的控制。

补丁有一些限制,这些限制与 git 本身的限制紧密相关:git 中最小的变化单位是一行,因此如果我们对一行进行了多次更改,这些更改代表了不同的工作单元,git 无法帮助我们分别记录这些更改。

但这只能说明,交互式补丁模式并不能替代或组织一个有序的开发流程。虽然它可以帮助我们从偶尔未能按照任务或团队要求的那样严格执行 git 纪律的会话中恢复过来,但它和任何工具一样,也可能被误用或过度使用。所以,不要让它取代git add -p你工作流程中的思考和规划。

鏂囩珷鏉ユ簮锛�https://dev.to/krnsk0/a-thorough-introduction-to-git-s-interactive-patch-mode-4bl6
PREV
如何成为高级开发人员
NEXT
JavaScript 入门 - 终极免费资源 GenAI LIVE! | 2025 年 6 月 4 日