跳转至

4.暂存区

在前几章中,你已经对Git的暂存区域有了一些了解。你已经学会了如何修改你的文件,为仓库添加新的文件,查看工作树和暂存区之间的差异,你甚至还尝到了git log的滋味。

但暂存区不仅仅是这几个操作。在这一点上,你可能想知道为什么要有暂存区。"你可能会问:"为什么你不能直接把所有的当前更新推送到版本库中?这是一个很好的问题,但这种线性方法存在一些问题;Git实际上是为了解决其他版本控制系统下存在的直接提交历史的一些常见问题而设计的。

在这一章中,你会了解到更多关于Git的暂存区域是如何工作的,为什么它是必要的,如何撤销你在暂存区域所做的修改,如何移动和删除仓库中的文件,等等。

为什么存在分期

开发是一个混乱的过程。理论上说,这应该是一个线性的、累积的代码功能构造,但更多的时候是一系列交织在一起的、非线性的死胡同代码、部分完成的功能、存根的测试、代码中的//TODO:注释集合,以及其他由人类驱动的、主要由手工制作的过程所固有的东西。

想到你一次只做一个特性或bug,你的工作树上只有干净的、有完整文档的代码,你的工作树上不会有不必要的文件,你的开发环境的配置总是与你的团队其他成员完美同步,你在调查bug时不会跟踪任何兔子的足迹(或创造一些自己的足迹),这是高贵的。

Git是为了弥补这种混乱的、非线性的开发方式而建立的。我们可以同时处理多的事情,并有选择地选择你想要的阶段并提交到仓库。一般的理念是,提交应该是一个有逻辑的变化集合,作为一个单元是有意义的--而不仅仅是"我更新的东西的最新集合,可能是也可能是不相关的"。

一个简单的分期例子

在下面的例子中,我正在做一个网站,我想让我的设计大师来审查我的CSS修改。我在工作过程中改变了以下文件:

index.html

images/favicon.ico
images/header.jpg
images/footer.jpg
images/profile.jpg

styles/admin.css
styles/frontend.css

scripts/main.js
scripts/admin.js
scripts/email.js

我在这里更新了很多文件,而不仅仅是CSS。如果我必须一次提交我在工作目录中修改的所有文件,我就会把所有东西都塞进一次提交:

img

如果我在做每一个小改动时都提交,我的提交历史可能会是下面这样:

img

然后,当我的设计大师想看一下CSS的变化时,她就不得不涉足我的提交信息,并有可能查看我的差异,甚至在Slack上呼唤我,以弄清她应该查看哪些文件。

但是,如果我先提交HTML的修改,然后是图片的修改,接着是JavaScript的修改,最后是CSS的修改,那么提交历史,甚至是我所做的事情的心理图景,就会变得清晰:

img

在本书后面的章节中,你会了解到能够有意识地选择各种修改进行提交,甚至只选择一个文件的一部分进行提交的力量。但现在,你将探索一些更常见的情况,包括移动文件、删除文件,甚至撤销你还没有好提交的修改。

撤销阶段性修改

你经常会改变你对一组特定的阶段性修改的看法,或者你甚至可能使用像git add .这样的东西,然后意识到其中有一些你不太愿意的阶段性修改。

你已经有了一个关于图书创意的文件,但你也想捕捉一些关于非技术性管理书籍的创意。看来,不是每个人都想学习如何编程。

回到你的终端程序,在books目录下创建一个新文件,命名为management_book_ideas.md

touch books/management_book_ideas.md

但是,等等--视频制作团队呼唤你,并紧急要求你更新视频内容创意文件,因为他们刚刚找到人制作"塞班入门"课程,而且,哦,你能不能把"高级MOS 6510编程"也加入到列表中?

好吧,不是什么大问题。打开videos/content_ideas.md,在"开始使用Symbian"条目中的括号内打上"x"标记为完成,并在"高级MOS 6510编程"条目的最后加上一行。当你完成后,你的文件应该是这样的:

# Content Ideas

Suggestions for new content to appear as videos:

[x] Beginning Pascal
[ ] Mastering Pascal
[x] Getting started with Symbian
[ ] Coding for the Psion V
[ ] Flash for developers
[ ] Advanced MOS 6510 Programming

现在,执行以下命令,将这些最近的变化添加到你的暂存区:

git add .

执行下面的命令,看看Git是如何看待当前的状况的:

git status

你应该看到以下内容:

On branch main
Your branch is ahead of 'origin/main' by 3 commits.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
  new file:   books/management_book_ideas.md
  modified:   videos/content_ideas.md

哦,糟了。你不小心添加了空的books/management_book_ideas.md。你可能还不想提交这个文件,是吗?好吧,现在你陷入了困境。既然有东西在暂存区,你该怎么把它弄走?

幸运的是,由于Git了解到目前为止的所有变化,它可以很容易的为你恢复你的修改。最简单的方法是通过 git reset来实现。

git reset

执行下面的命令,从暂存区删除对books/management_book_ideas.md的修改:

git reset HEAD books/management_book_ideas.md

git reset将你的环境恢复到一个特定的状态。但是,等等 - 这个HEAD是什么意思?

HEAD是一个简单的标签,引用最近的提交。你可能已经注意到,在本书的早期部分,你的控制台输出中出现了HEAD一词。

如果你错过了,可以执行下面的命令来查看日志:

git log

如果你看一下控制台中输出的最上面几行,你会看到与下面类似的东西:

commit 6c88142dc775c4289b764cb9cf2e644274072102 (HEAD -> main)
Author: Chris Belanger <chris@razeware.com>
Date:   Sat Jan 19 07:16:11 2019 -0400

    Adding some tutorial ideas

那个(HEAD -> main)的注释告诉你,你在本地系统上的最新提交和你所期望的一样,就是你添加了那些教程想法的那个提交,而且这个提交是main分支上完成的。本节后面会提到分支,但现在只需理解HEAD会记录您的最新提交。

所以,git reset HEAD books/management_book_ideas.md,在这里意味着"使用HEAD作为参考点,将暂存区域恢复到该点,但只恢复与books/management_book_ideas.md文件相关的任何改动"。

要看到实际情况是这样的,必要时用Q退出git log,并再次执行git status

~/GitApprentice/ideas $ git status
On branch main
Your branch is ahead of 'origin/main' by 3 commits.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
  modified:   videos/content_ideas.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
  books/management_book_ideas.md

这看起来好多了。Git不再追踪books/management_book_ideas.md,但它仍在追踪你对videos/content_ideas.md的修改。呼--你回到了你想去的地方。

在你陷入更多麻烦之前,最好提交最后的修改。执行下面的命令来添加另一个提交:

git commit -m "Updates book ideas for Symbian and MOS 6510"

现在,你想了一下,你认为你不应该把那些关于视频平台本身的想法放在videos文件夹中。他们更适合属于一个新的文件夹。website

Git中移动文件

用下面的命令为网站的想法创建文件夹:

mkdir website

现在,你需要把这个文件从videos目录移到website目录。即使你对Git的经验不多,你也可能怀疑这不是简单的把文件从一个目录移到另一个。这是正确的,但看看为什么会这样,会有启发。

所以,你先用暴力方式移动它,看看Git如何解释你的行动。执行下面的命令,使用标准的mv命令行工具,将文件从一个目录移到另一个目录:

mv videos/platform_ideas.md website/

现在,执行git status来看看Git对你所做的事情的看法:

~/GitApprentice/ideas $ git status
On branch main
Your branch is ahead of 'origin/main' by 4 commits.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
  deleted:    videos/platform_ideas.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
  books/management_book_ideas.md
  website/

no changes added to commit (use "git add" and/or "git commit -a")

嗯,这有点乱。Git认为你删除了一个被追踪的文件,而且它还认为你添加了这个website的胡话。毕竟,Git看起来并不那么聪明。为什么它不直接看到你已经移动了文件?

答案是Git对文件的思考方式:作为完整的路径,而不是单独的目录。看看Git在移动之前是如何看待工作树的这一部分的:

videos/platform_ideas.md (tracked)
videos/content_ideas.md (tracked)

而且,在移动之后,它看到的情况是这样的:

videos/platform_ideas.md (deleted)
videos/content_ideas.md (tracked)
website/platform_ideas.md (untracked)

记住,Git对目录一无所知。它只知道完整的路径。对比上面两个工作树的片段,你会发现为什么git status会报告它所做的。

似乎mv的蛮力方法并不是你想要的。Git有一个内置的mv命令来为你"properly"移动东西。

用下面的命令把文件移回来:

mv website/platform_ideas.md videos/

现在,执行以下内容:

git mv videos/platform_ideas.md website/

并执行git status,看看有什么进展:

~/GitApprentice/ideas $ git status
On branch main
Your branch is ahead of 'origin/main' by 4 commits.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
  renamed:    videos/platform_ideas.md -> website/platform_ideas.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
  books/management_book_ideas.md

这看起来好多了。Git认为这个文件是"重命名的",这是有道理的,因为Git是按全路径来考虑文件的。而且,Git还为你安排了这个改动。很好!

现在提交这些修改:

git commit -m "Moves platform ideas to website directory"

你的想法项目现在看起来很像船形。但是,说实话,那些直播的想法非常糟糕。也许你应该在太多的人看到它们之前就把它们扔掉。

Git中删除文件

像平时在文件系统中那样删除/移动/重命名文件的冲动,通常会使Git陷入混乱,并导致人们说他们不"懂"Git。但如果你花时间指导Git做什么,它通常会很好地帮你处理事情。

所以--那个现场直播的想法文件必须要去。正如你所猜测的那样,这种粗暴的方法并不是解决问题的最好方法,但让我们看看它是否会给Git带来麻烦。

执行下面的命令,用rm命令删除直播创意文件:

rm articles/live_streaming_ideas.md

然后执行 git status 来看看Git的反应:

~/GitApprentice/ideas $ git status
On branch main
Your branch is ahead of 'origin/main' by 5 commits.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
  deleted:    articles/live_streaming_ideas.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
  books/management_book_ideas.md

no changes added to commit (use "git add" and/or "git commit -a")

哦,这还不算太糟。Git识别出你已经删除了文件,并提示你将其归档。

现在用下面的命令来做吧:

git add articles/live_streaming_ideas.md

然后,看看git status是什么情况:

~/GitApprentice/ideas $ git status
On branch main
Your branch is ahead of 'origin/main' by 5 commits.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
  deleted:    articles/live_streaming_ideas.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
  books/management_book_ideas.md

好吧,那是一个有点迂回的方式来做事情。但就像git mv一样,你可以使用git rm命令来一举完成。

恢复被删除的文件

首先,你需要回到你原来的位置。用你最好的新朋友git reset来解除对流媒体直播创意文件的修改:

git reset HEAD articles/live_streaming_ideas.md

Git明确地告诉你,它已经在你刚刚做的reset之后取消了文件的缓存:

Unstaged changes after reset:
D   articles/live_streaming_ideas.md

这将从暂存区域移除该修改 - 但它并没有恢复工作树上的文件本身。要做到这一点,你需要告诉Git从仓库中获取该文件的最新提交版本。

执行下面的命令来恢复你的文件,使其恢复原来的名声:

git checkout HEAD articles/live_streaming_ideas.md

你就回到了你开始的地方。你可以通过Git在执行上述命令后告诉你的情况来验证文件是否被恢复了:

Updated 1 path from b80985f

现在,用下面的命令摆脱该文件:

git rm articles/live_streaming_ideas.md

最后,用一个适当的信息提交该变化:

git commit -m "Removes terrible live streaming ideas"

看起来你不得不把直播留给专家:YouTube上的14岁孩子,他们有太多的时间,而常识又太少。

那个关于管理书籍想法的空文件还在附近挂着。既然你还没有任何好的想法,你不妨把它提交出去,希望以后有人能用好的方法来充实它,成为一个有效的管理者。

用下面的命令添加那个空文件:

git add books/management_book_ideas.md

并用一个好的评论来承诺:

git commit -m "Adds all the good ideas about management"

这并不全是坏事。放弃在直播领域建立事业的尝试,和管理,让你有更多的时间去迎接下一个挑战!

挑战

挑战:移动、删除和恢复一个文件

这一挑战将带你体验你刚刚学到的东西。你需要做以下工作:

  1. git mv命令将新添加的books/management_book_ideas.md移到website目录。
  2. 你改变了主意,不再需要management_book_ideas.md,所以用git rm命令完全删除该文件。当你这样做时,Git会给你一个错误,但仔细看看错误中的建议操作,看看如何用-f选项解决这个问题,然后再试试。
  3. 但现在你有了新的想法。也许你有一些关于管理的好主意。将该文件恢复到原来的位置。

记得在需要的时候使用git status命令来确定你的方向。自由使用git status肯定会帮助你了解Git在这个挑战的每个阶段在做什么。

如果你被卡住了,或者想检查你的解决方案,你可以在本章的challenge文件夹下找到这个挑战的答案。

关键点

  • staging area让你以一种逻辑的、结构化的方式构建你的下一次提交。
  • git reset HEAD <filename>让你的暂存环境恢复到最后一次提交的状态。
  • 在不通知Git的情况下,随意移动文件并从文件系统中删除它们,会给你带来麻烦。
  • git mv 移动文件,并将更改分阶段进行,都是一个动作。
  • git rm从你的仓库中删除文件,并对更改进行分段,同样是一个动作。
  • git reset HEAD <filename>git checkout HEAD <filename>来恢复被删除的文件和阶段性文件。

接下来去哪里?

这是个不错的旅程你已经深入了解了Git是如何看待这个世界的;当你在日常工作流程中更多地使用Git时,建立一个平行的心理模型将对你有很大的帮助。

有时,你可能会有一些文件,你明确地不想添加到你的仓库中,但你想在工作树中保留。你可以告诉Git忽略工作树中的东西,甚至告诉Git忽略你所有项目中的特定文件,这就是所谓的.gitignore文件的魔力--你将在下一章中了解它的全部内容!