章节 ▾ 第二版

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 add 命令时,Git 会精确地暂存文件当时的版本。如果你现在提交,CONTRIBUTING.md 文件在你上次运行 git add 命令时的版本将被提交,而不是你在运行 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 自动添加,甚至不希望它显示为未跟踪。这些通常是自动生成的文件,例如日志文件或由你的构建系统生成的文件。在这种情况下,你可以创建一个文件,列出匹配这些文件的模式,并将其命名为 .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 查看程序,还有另一种方式来查看这些差异。如果你运行 git difftool 而不是 git diff,你可以在 emerge、vimdiff 等许多软件(包括商业产品)中查看这些差异。运行 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 提供了一个简单的快捷方式。在 git commit 命令中添加 -a 选项会使 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(-)

请注意,在这种情况下,在提交之前,你无需对 CONTRIBUTING.md 文件运行 git add。那是因为 -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

请注意 * 前面的反斜杠 (\)。这是必要的,因为除了 shell 的文件名扩展外,Git 还会进行自己的文件名扩展。此命令会删除 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 操作。

scroll-to-top