Skip to content
This page has been auto-translated and may contain errors.View in English

提交循环

docs.scrimba.com

假设你在 weather-app 上工作了最后二十分钟。你在 src/index.js 中修复了一个真实的bug,温度转换有偏差,当你在那里时,你还开始重新设计 styles.css,但那个更改还未完成,还没有准备好让其他人看到。你想现在就将bug修复保存为它自己的检查点,而不需要将未完成的样式变更一起拖入。

这正是提交循环的用途。如果你还没有设置仓库,你的第一个仓库会引导你了解 git init 和克隆。从这里开始,本章假设你已经打开了一个仓库。

三个阶段:工作目录、暂存区、提交

在 Git 中,你所做的每一个更改在成为永久历史之前都会经过三个阶段。你的工作目录是项目在磁盘上的当前状态,是你正在编辑的文件。暂存区是一个临时位置,你在这里放置确切地想在下一步保存的更改。一个提交是已保存的快照本身,一旦你写入它,加上描述它的消息。

想象为搬家打包。你的整个房子是工作目录:你拥有的一切,无论其状态如何。你已经打包并用胶带封好放在前门的盒子是暂存区:只有你故意选择带走的东西。装着那些盒子开走的搬家车是提交:一条记录,记录了确切地在那一刻离开的东西,外面有一个标签说明里面是什么。

这是这个心理模型在实际终端中的展示。在编辑 weather-app 中的两个文件后,git status 读取当前状态而不改变任何东西:

bash
$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/index.js
        modified:   styles.css

no changes added to commit (use "git add" to commit)

两个文件都显示为"未暂存",因为编辑文件只会改变你的工作目录。除非你用 git add 告诉 Git 把它放在那里,否则没有任何东西会进入暂存区,这是下一部分。

这是暂存区存在的全部理由变得清晰的地方。暂存区让你精确地选择进入提交的内容,独立于工作目录中还有什么其他已编辑的东西。没有它,保存一个检查点会强制你一次性获取磁盘上的每个更改,无论是否准备好。

有了它,你可以暂存 src/index.js 中完成的bug修复,排除半完成的 styles.css,并仅提交实际完成的部分。这一个细节,暂存和你的工作目录可以同时持有不同的东西,让几乎每个人在第一周都会困惑。一旦它有意义了,Git 的其余日常工作流程就会变得更加清晰。

在日常工作中,在每次 add 和每次 commit 之前,无论是否有什么感觉不对,都要养成 git status 的习惯。它没有成本,它永远不会改变你的项目,它阻止了提交超过你想要的常见错误。养成检查状态、暂存,然后在提交前再次检查状态的习惯:这只需要几秒钟,这意味着你提交中的消息总是与实际进入的内容相匹配。

暂存区是一个真实的文件,称为**索引**,位于你的仓库内的 .git/index。每次你运行 git add 时,Git 都会更新该文件以记录哪个版本的哪个文件被暂存。当你运行 git commit 时,Git 纯粹从索引说的内容构建新提交,而不再检查你的工作目录。这是你刚才在 git status 中看到的整个分割背后的机制:磁盘上编辑了两个文件,但索引还没有跟踪其中任何一个,所以随后的提交目前将不包含任何内容。

JunoGit 的三个区域 你的工作目录是你的文件现在的样子。暂存区是你放置下一步想要保存的确切更改的地方,提交是一旦你写入它的已保存快照。编辑文件本身不会自动将其暂存,你用 git add 选择要进入的内容,这就是让你保存一个完成的更改而排除一个半完成的更改的方式。
JunoGit 的三个区域 工作目录、暂存区、提交:编辑存在于第一个,git add 将你选择的确切内容移动到第二个,git commit 将第二个保存为永久快照。在暂存前运行 git status,在提交前再次运行,它是免费的,它会保持你的提交与你想要保存的内容相匹配。
JunoGit 的三个区域 暂存区是一个真实的文件,位于 .git/index 的索引,git commit 从索引构建其快照,永远不会直接从你的工作目录。这就是为什么两个编辑的文件都可以显示为未暂存的原因:索引还没有被告知其中任何一个。以这种方式理解索引会让每个暂存命令感觉不那么神奇,更像是读取和写入一个文件。

暂存更改:git add

git add 将更改从你的工作目录移动到暂存区。指向想要进入下一个提交的文件:

bash
$ git add src/index.js
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   src/index.js

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   styles.css

src/index.js 移动到"要提交的更改",bug修复已暂存并准备好。styles.css 仍然列在"未暂存"下,这正是你在重新设计未完成时想要的地方。同样运行 git add styles.css 也会暂存它;运行 git add . 一次暂存当前文件夹中的每个更改文件,一旦你相信磁盘上的一切都真的准备好了,这就很方便。

有时单个文件有你想保存的更改和你不想保存的更改,将其分割成两个文件不太实际。git add -p <file>--patch 的缩写)会遍历文件的更改,分成称为块的小段,并询问你对每个块的决定:

bash
$ git add -p src/index.js
@@ -12,7 +12,7 @@ function celsiusToFahrenheit(celsius) {
-  return celsius * (9 / 5 + 32)
+  return (celsius * 9) / 5 + 32
Stage this hunk [y,n,q,a,d,e,?]?

对该块回答 y 以暂存它,n 留作以后使用,q 停止。这给了你提交大小的控制,即使在单个文件内,当"这个文件的一半完成"描述你的情况时,这值得去做。

git add 同时做两件事,知道两者都解释了你最终会遇到的一个陷阱。首先,它将文件的当前内容写入一个新的**blob**,一个 Git 永久存储的对象,由与提交相同的方式识别的哈希,保存一个文件在一个时刻的确切内容。其次,它更新索引以指向该文件的那个 blob。add 之后就不会再看那个文件了。

这就是为什么,如果你 git add src/index.js 然后继续编辑同一个文件在提交之前,git status 显示它同时被暂存和修改:索引仍然指向你运行 add 时的 blob,而你的工作目录已经向前移动。git commit 只看到暂存的 blob,所以第二个 git add 是在保存之前拉入更新的编辑的方式。

Juno暂存更改git add <file> 将该文件的当前更改移动到暂存区,为下一个提交做准备。你还没有添加的文件留在外面,所以你可以完成一件事并留下另一件处于中途编辑的状态。git add . 一次暂存每个更改的文件,一旦一切都真的准备好了就很方便。
Juno暂存更改git add <file> 暂存整个文件,git add -p <file> 让你在只有文件的一部分准备好时逐块暂存它。当"这个文件的一半完成"为真时,第二个是要去的工具。
Juno暂存更改git add 将你的文件当前内容写入 blob 并使索引指向它,它之后不会监视该文件。在提交前再次编辑该文件,status 显示它既被暂存又被修改,因为索引仍然保存较旧的 blob。重新运行 add 以拉入较新的编辑。这第一次碰到它时让我困惑了十分钟。

保存快照:git commit

一旦暂存区持有你想要的东西,git commit 将其保存为永久快照,-m 标志("message" 的缩写)提供随之而来的描述:

bash
$ git commit -m "Fix temperature conversion in Celsius-to-Fahrenheit formula"
[main 4f2a1c9] Fix temperature conversion in Celsius-to-Fahrenheit formula
 1 file changed, 1 insertion(+), 1 deletion(-)

只有 src/index.js 进入了,因为那是你暂存的全部。styles.css 仍然在你的工作目录中进行编辑,未触及,等待什么时候重新设计完成。短 id 4f2a1c9 命名这个确切的提交,历史章节涵盖了读取像这样的项目的完整提交列表。

以祈使语气写提交消息,作为一条指令。"Fix temperature conversion" 与 Git 本身使用的措辞相匹配("这个提交将...")并在列出一行提交的工具中清晰地读取。对于任何超过单行修复的内容,添加一个解释为什么需要更改的正文。diff 已经显示了更改的内容,所以将正文留给它背后的推理:

bash
$ git commit -m "Fix temperature conversion in Celsius-to-Fahrenheit formula

The formula was applying operator precedence in the wrong order,
which rounded warm temperatures down by a degree in the UI."

如果你在提交后注意到拼写错误或遗漏的细节,git commit --amend 用新提交替换你最近的提交,而不是在顶部添加另一个:

bash
$ git commit --amend -m "Fix temperature conversion in Celsius-to-Fahrenheit formula"

仅修改你还没有在任何地方共享的提交。一旦你推送了它(将其发送到项目的共享副本),修改会重写其他人可能已经拥有的历史,撤销事情涵盖了之后修复提交的安全方式。

一个提交本身是一个有三个部分的小对象:指向一个****的指针、作者和消息,以及指向其父提交的指针。树是你整个项目目录结构在那一刻的快照:对于每个文件和文件夹,它记录一个名称和指向 blob(文件内容)或另一个树(子文件夹)的指针。运行 git add 一次写入一个文件的 blob 并更新索引;git commit 是 Git 将当前索引转换为一个完成的树并将其包装在提交对象中的时刻。

两个具有相同内容的文件指向完全相同的 blob,即使位于不同的文件夹中,所以提交永远不会存储这些字节两次。该结构也使比较两个提交高效:Git 并排走它们的两个树,仅打开指针实际不同的 blob,跳过保持不变的每个文件。

Juno保存提交git commit -m "message" 将当前暂存的任何内容保存为永久快照,带有你给它的消息。你没有暂存的任何东西都保持不变并可编辑。保持消息简短清楚关于提交做什么,未来你会比你预期的更经常读取它。
Juno保存提交 将提交消息写成指令,"Fix" 而不是 "Fixed",并在 diff 本身会让人猜测你为什么做出更改时添加正文。git commit --amend 替换你的最后一个提交而不是堆积新的,这对于消息拼写错误是完美的,只要你还没有将该提交推送到任何地方。
Juno保存提交 一个提交指向一个树,一个树映射名称到 blob 和进一步的树,从索引在提交时持有的任何东西构建项目的完整嵌套快照。add 写入 blob,commit 将索引转换为完成的树,项目中任何地方的相同内容共享一个 blob 而不是被存储两次。想象那个形状,两件事停止感觉神奇:为什么没有任何东西被重复,以及为什么比较两个提交意味着并排走两个树而不是重新读取每个文件。