章节 ▾ 第二版

2.2 Git基础 - 记录更改到仓库

记录更改到仓库

此时,你的本地计算机上应该有一个正规的 Git 仓库,并且你面前有一个包含所有文件的检出或工作副本。通常,你希望在项目达到某个需要记录的状态时,就开始进行更改并将这些更改的快照提交到你的仓库中。

请记住,工作目录中的每个文件都可以处于跟踪未跟踪两种状态之一。跟踪的文件是上次快照中存在的文件,以及任何新暂存的文件;它们可以是未修改、已修改或已暂存的。简而言之,跟踪的文件是 Git 已知的那些文件。

未跟踪的文件是其他所有文件——即工作目录中不在上次快照中且不在暂存区的任何文件。当你首次克隆仓库时,所有文件都将是跟踪的且未修改的,因为 Git 刚刚检出它们,而你尚未编辑任何内容。

当你编辑文件时,Git 会将它们视为已修改,因为你自上次提交以来已更改了它们。在你工作时,你会选择性地暂存这些已修改的文件,然后提交所有这些暂存的更改,循环往复。

The lifecycle of the status of your files
图 8. 文件状态的生命周期

查看文件状态

用于确定文件处于哪种状态的主要工具是 git status 命令。如果你在克隆后直接运行此命令,应该会看到类似以下内容

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

这意味着你有一个干净的工作目录;换句话说,你跟踪的文件都没有被修改。Git 也看不到任何未跟踪的文件,否则它们会在此列出。最后,该命令会告诉你你当前在哪个分支上,并告知你该分支尚未与服务器上的同一个分支发生分歧。目前,该分支始终是 master,这是默认设置;你在这里不必担心它。有关分支和引用的详细信息,请参见 Git 分支

注意

GitHub 在 2020 年中期将默认分支名称从 master 更改为 main,其他 Git 主机也纷纷效仿。因此,你可能会发现某些新创建的仓库的默认分支名称是 main 而不是 master。此外,默认分支名称可以更改(如 你的默认分支名称 中所示),因此你可能会看到默认分支的名称不同。

但是,Git 本身仍使用 master 作为默认值,因此我们在本书的其余部分将继续使用它。

假设你为项目添加了一个新文件,一个简单的 README 文件。如果该文件以前不存在,并且你运行 git status,你会看到如下所示的未跟踪文件

$ echo 'My Project' > README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    README

nothing added to commit but untracked files present (use "git add" to track)

你可以看到你的新 README 文件是未跟踪的,因为它出现在状态输出的“Untracked files”(未跟踪的文件)标题下。未跟踪基本上意味着 Git 发现了一个文件,该文件不在你之前的快照(提交)中,并且尚未暂存;Git 不会开始将其包含在你的提交快照中,直到你明确告诉它这样做为止。这样做是为了防止你意外开始包含生成的二进制文件或其他你不打算包含的文件。你确实想开始包含 README,所以让我们开始跟踪该文件。

跟踪新文件

要开始跟踪一个新文件,你需要使用 git add 命令。要开始跟踪 README 文件,你可以运行这个命令

$ git add README

如果你再次运行状态命令,你会看到你的 README 文件现在已被跟踪并暂存,准备提交

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

    new file:   README

你可以通过它出现在“Changes to be committed”(待提交的更改)标题下来判断它已被暂存。如果你此时提交,运行 git add 时文件的版本将成为后续历史快照的一部分。你可能还记得,当你之前运行 git init 时,接着运行了 git add <files> —— 那是为了开始跟踪目录中的文件。git add 命令接受文件或目录的路径名;如果它是目录,该命令会递归地添加该目录中的所有文件。

暂存已修改的文件

让我们修改一个已跟踪的文件。如果你修改了一个先前已跟踪的文件,名为 CONTRIBUTING.md,然后再次运行 git status 命令,你会得到类似以下内容

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README

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

    modified:   CONTRIBUTING.md

CONTRIBUTING.md 文件出现在一个名为“Changes not staged for commit”(未暂存的更改)的部分——这意味着一个已跟踪的文件在工作目录中被修改但尚未暂存。要暂存它,你需要运行 git add 命令。git add 是一个多功能命令——你用它来开始跟踪新文件、暂存文件,以及执行其他操作,例如标记合并冲突的文件已解决。将其视为“将此精确内容添加到下一次提交”而不是“将此文件添加到项目”可能更有帮助。现在让我们运行 git add 来暂存 CONTRIBUTING.md 文件,然后再次运行 git status

$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

两个文件都被暂存,并将包含在你下一次提交中。此时,假设你记得在提交 CONTRIBUTING.md 之前要进行一个小小的更改。你再次打开它并进行更改,准备提交。然而,让我们再运行一次 git status

$ vim CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

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

    modified:   CONTRIBUTING.md

这是怎么回事?现在 CONTRIBUTING.md 同时被列为已暂存未暂存。这怎么可能?事实证明,Git 会在你运行 git add 命令时精确地暂存文件。如果你现在提交,在你上次运行 git add 命令时的 CONTRIBUTING.md 版本将是提交的内容,而不是你在运行 git commit 时看到的你工作目录中的文件版本。如果你在运行 git add 后修改了文件,你必须再次运行 git add 来暂存文件的最新版本

$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

简短状态

虽然 git status 的输出相当全面,但也相当冗长。Git 还提供了一个简短状态标志,让你能以更紧凑的方式查看你的更改。如果你运行 git status -sgit status --short,你会得到一个更简化的命令输出

$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

新未跟踪的文件旁边有一个 ??,已添加到暂存区的新文件有一个 A,已修改的文件有一个 M,依此类推。输出有两列——左列表示暂存区的状态,右列表示工作树的状态。例如,在该输出中,README 文件在工作目录中已被修改但尚未暂存,而 lib/simplegit.rb 文件已被修改并暂存。Rakefile 被修改、暂存,然后再次修改,因此它既有暂存的更改,也有未暂存的更改。

忽略文件

很多时候,你会有一类文件,你不想让 Git 自动添加,甚至不想让 Git 将它们显示为未跟踪。这些通常是自动生成的文件,如日志文件或由你的构建系统生成的文件。在这种情况下,你可以创建一个包含匹配这些文件的模式的文件,命名为 .gitignore。这是一个 .gitignore 文件示例

$ cat .gitignore
*.[oa]
*~

第一行告诉 Git 忽略任何以“.o”或“.a”结尾的文件——这些是对象文件和归档文件,可能是代码构建的产物。第二行告诉 Git 忽略所有文件名以波浪线 (~) 结尾的文件,许多文本编辑器(如 Emacs)使用它来标记临时文件。你也可以包含 log、tmp 或 pid 目录;自动生成的文档;等等。为你的新仓库设置一个 .gitignore 文件通常是个好主意,这样你就不会意外提交你真正不想放在 Git 仓库中的文件。

.gitignore 文件中可以使用的模式规则如下

  • 空行或以 # 开头的行将被忽略。

  • 标准的 glob 模式有效,并将递归地应用于整个工作树。

  • 模式可以以斜杠 (/) 开头以避免递归。

  • 模式可以以斜杠 (/) 结尾以指定一个目录。

  • 可以通过在模式前加上感叹号 (!) 来否定该模式。

Glob 模式类似于 shell 使用的简化正则表达式。星号 (*) 匹配零个或多个字符;[abc] 匹配方括号内的任何字符(本例中为 a、b 或 c);问号 (?) 匹配单个字符;方括号包含用连字符分隔的字符([0-9])匹配其中的任何字符(本例中为 0 到 9)。你还可以使用两个星号来匹配嵌套目录;a/**/z 将匹配 a/za/b/za/b/c/z,依此类推。

这是另一个 .gitignore 文件示例

# ignore all .a files
*.a

# but do track lib.a, even though you're ignoring .a files above
!lib.a

# only ignore the TODO file in the current directory, not subdir/TODO
/TODO

# ignore all files in any directory named build
build/

# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt

# ignore all .pdf files in the doc/ directory and any of its subdirectories
doc/**/*.pdf
提示

GitHub 在 https://github.com/github/gitignore 上维护了一份相当全面的常用 .gitignore 文件示例列表,涵盖了数十种项目和语言,可以作为你项目的起点。

注意

在简单的情况下,一个仓库可能在其根目录中有一个单独的 .gitignore 文件,该文件会递归地应用于整个仓库。但是,在子目录中拥有额外的 .gitignore 文件也是可能的。这些嵌套 .gitignore 文件中的规则仅适用于它们所在目录下的文件。Linux 内核源代码仓库有 206 个 .gitignore 文件。

此书的范围不涉及多个 .gitignore 文件的详细信息;详情请参阅 man gitignore

查看已暂存和未暂存的更改

如果 git status 命令对你来说太笼统——你想要确切地知道你更改了什么,而不仅仅是哪些文件被更改了——你可以使用 git diff 命令。我们将在后面更详细地介绍 git diff,但你可能会经常使用它来回答这两个问题:你更改了但尚未暂存的内容是什么?你已暂存但即将提交的内容是什么?虽然 git status 通过列出文件名来非常笼统地回答这些问题,但 git diff 会向你显示确切添加和删除的行——即所谓的补丁。

假设你再次编辑并暂存了 README 文件,然后编辑了 CONTRIBUTING.md 文件但没有暂存它。如果你运行 git status 命令,你会再次看到类似以下内容

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   README

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

    modified:   CONTRIBUTING.md

要查看你已更改但尚未暂存的内容,请键入 git diff,不带任何其他参数

$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
 Please include a nice description of your changes when you submit your PR;
 if we have to read the whole diff to figure out why you're contributing
 in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.

 If you are starting to work on a particular area, feel free to submit a PR
 that highlights your work in progress (and note in the PR title that it's

该命令会将你工作目录中的内容与暂存区中的内容进行比较。结果会告诉你尚未暂存的更改。

如果你想查看你已暂存但将包含在你下一次提交中的内容,你可以使用 git diff --staged。此命令将你的暂存更改与上次提交进行比较

$ git diff --staged
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+My Project

重要的是要注意,git diff 本身并不显示自上次提交以来所有的更改——只显示尚未暂存的更改。如果你已暂存了所有更改,git diff 将不会有任何输出。

再举个例子,如果你暂存了 CONTRIBUTING.md 文件然后又编辑了它,你可以使用 git diff 来查看已暂存的更改和未暂存的更改。如果我们的环境看起来像这样

$ git add CONTRIBUTING.md
$ echo '# test line' >> CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   CONTRIBUTING.md

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

    modified:   CONTRIBUTING.md

现在你可以使用 git diff 来查看哪些内容仍未暂存

$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 643e24f..87f08c8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -119,3 +119,4 @@ at the
 ## Starter Projects

 See our [projects list](https://github.com/libgit2/libgit2/blob/development/PROJECTS.md).
+# test line

以及 git diff --cached 来查看你迄今为止已暂存的内容(--staged--cached 是同义词)

$ git diff --cached
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
 Please include a nice description of your changes when you submit your PR;
 if we have to read the whole diff to figure out why you're contributing
 in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.

 If you are starting to work on a particular area, feel free to submit a PR
 that highlights your work in progress (and note in the PR title that it's
注意
外部工具中的 Git Diff

在本书的其余部分,我们将以各种方式继续使用 git diff 命令。如果你更喜欢图形化或外部的 diff 查看程序,还有另一种查看这些 diff 的方法。如果你运行 git difftool 而不是 git diff,你可以在 emerge、vimdiff 等软件(包括商业产品)中查看任何 diff。运行 git difftool --tool-help 来查看你系统上可用的工具。

提交你的更改

现在你的暂存区已按你想要的方式设置好,你可以提交你的更改了。请记住,任何尚未暂存的内容——任何你创建或修改但自上次编辑以来尚未运行 git add 的文件——都不会包含在此次提交中。它们将作为已修改的文件保留在你的磁盘上。在这种情况下,假设你上次运行 git status 时,看到一切都已暂存,所以你已准备好提交你的更改。提交的最简单方法是键入 git commit

$ git commit

这样做会启动你选择的编辑器。

注意

这是由你的 shell 的 EDITOR 环境变量设置的——通常是 vim 或 emacs,尽管你可以使用 git config --global core.editor 命令将其配置为你想要的任何内容,如 入门 中所示。

编辑器将显示以下文本(此示例是 Vim 屏幕)

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
#	new file:   README
#	modified:   CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C

你可以看到默认的提交消息包含了注释掉的最新 git status 命令的输出以及上面的一行空行。你可以删除这些注释并键入你的提交消息,或者保留它们以帮助你记住你正在提交的内容。

注意

为了更明确地提醒你已修改的内容,你可以将 -v 选项传递给 git commit。这样做也会将你的更改的 diff 放入编辑器中,以便你可以确切地看到你正在提交的更改。

当你退出编辑器时,Git 将使用该提交消息(去除注释和 diff)创建你的提交。

或者,你可以通过在 commit 命令后指定一个 -m 标志来内联键入你的提交消息,如下所示

$ git commit -m "Story 182: fix benchmarks for speed"
[master 463dc4f] Story 182: fix benchmarks for speed
 2 files changed, 2 insertions(+)
 create mode 100644 README

现在你已经创建了你的第一个提交!你可以看到提交给了你一些关于它自身的输出:你提交到的分支(master),提交的 SHA-1 校验和(463dc4f),更改了多少文件,以及关于提交中添加和删除的行数的统计信息。

请记住,提交记录了你在暂存区中设置的快照。任何你未暂存的内容仍然在那里被修改;你可以进行另一次提交将其添加到你的历史记录中。每次执行提交时,你都在记录项目的一个快照,以便以后可以恢复或比较。

跳过暂存区

尽管暂存区对于精确地制作你想要的提交非常有用,但有时它比你的工作流程所需更复杂。如果你想跳过暂存区,Git 提供了一个简单的快捷方式。将 -a 选项添加到 git commit 命令,可以让 Git 在提交前自动暂存所有已跟踪的文件,让你跳过 git add 步骤

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -a -m 'Add new benchmarks'
[master 83e38c7] Add new benchmarks
 1 file changed, 5 insertions(+), 0 deletions(-)

注意在这种情况下,你不需要在提交之前运行 git add on the CONTRIBUTING.md 文件。这是因为 -a 标志包含了所有已更改的文件。这很方便,但要小心;有时这个标志会导致你包含不需要的更改。

删除文件

要从 Git 中删除一个文件,你需要将其从跟踪的文件中移除(更准确地说,从暂存区中移除),然后提交。git rm 命令执行此操作,同时也会从你的工作目录中删除该文件,以便下次不会将其显示为未跟踪的文件。

如果你只是从工作目录中删除了文件,它会出现在你的 git status 输出的“Changes not staged for commit”(未暂存的更改)区域(也就是说,未暂存

$ rm PROJECTS.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        deleted:    PROJECTS.md

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

然后,如果你运行 git rm,它会暂存文件的删除

$ git rm PROJECTS.md
rm 'PROJECTS.md'
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    PROJECTS.md

下次提交时,文件将消失,不再被跟踪。如果你修改了文件或已将其添加到暂存区,你必须使用 -f 选项强制删除。这是一个安全功能,旨在防止意外删除尚未记录在快照中且无法从 Git 恢复的数据。

你可能还想做的另一件事是保留工作树中的文件,但将其从暂存区移除。换句话说,你可能想在硬盘上保留该文件,但不再让 Git 跟踪它。如果你忘记将某项添加到 .gitignore 文件中并意外暂存了它,例如一个大的日志文件或一堆编译好的 .a 文件,这一点尤其有用。要做到这一点,使用 --cached 选项

$ git rm --cached README

你可以将文件、目录和文件 glob 模式传递给 git rm 命令。这意味着你可以做一些事情,比如

$ git rm log/\*.log

注意 * 前面的反斜杠 (\)。这是必需的,因为 Git 除了 shell 的文件名扩展之外,还会进行自己的文件名扩展。此命令会删除 log/ 目录中所有以 .log 扩展名结尾的文件。或者,你可以这样做

$ git rm \*~

此命令会删除所有文件名以 ~ 结尾的文件。

移动文件

与其他许多 VCS 不同,Git 不会显式地跟踪文件移动。如果你在 Git 中重命名了一个文件,Git 中不会存储任何元数据来告诉它你重命名了该文件。然而,Git 在事后非常智能地识别出这一点——我们将在稍后处理文件移动的检测。

因此,Git 拥有一个 mv 命令会有点令人困惑。如果你想在 Git 中重命名一个文件,你可以运行类似下面的命令

$ git mv file_from file_to

这会正常工作。事实上,如果你运行类似这样的命令并查看状态,你会看到 Git 将其视为一个已重命名的文件

$ git mv README.md README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

然而,这相当于运行类似下面的命令

$ mv README.md README
$ git rm README.md
$ git add README

Git 会隐式地识别出这是一个重命名,所以你使用哪种方式重命名文件效果是一样的,无论是通过 mv 命令还是其他方式。唯一的真正区别是 git mv 是一个命令而不是三个——它是一个方便函数。更重要的是,你可以使用任何你喜欢的工具来重命名文件,并在提交之前再处理 add/rm