章节 ▾ 第二版

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 文件是未跟踪的,因为它在你的状态输出中的“未跟踪文件”标题下。未跟踪基本上意味着 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

你可以看出它已暂存,因为它在“要提交的更改”标题下。如果你此时提交,那么你运行 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 文件出现在名为“尚未暂存以提交的更改”部分——这意味着一个被跟踪的文件已在工作目录中被修改但尚未暂存。要暂存它,你运行 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 命令时,会精确地暂存文件当时的模样。如果你现在提交,那么 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 命令。如果你更喜欢图形化或外部差异查看程序,还有另一种查看这些差异的方式。如果你运行 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 命令的输出(注释掉)以及顶部的一个空行。你可以删除这些注释并输入你的提交消息,或者你可以将它们留在那里以帮助你记住你正在提交什么。

注意

为了更明确地提醒你修改了什么,你可以向 git commit 传递 -v 选项。这样做也会将你的更改的差异放入编辑器中,这样你就可以确切地看到你正在提交的更改。

当你退出编辑器时,Git 会使用该提交消息(删除注释和差异)创建你的提交。

或者,你可以在 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 输出的“尚未暂存以提交的更改”(即,*未暂存*)区域下

$ 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

你可以将文件、目录和文件通配符模式传递给 git rm 命令。这意味着你可以做这样的事情

$ git rm log/\*.log

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

$ git rm \*~

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

移动文件

与许多其他版本控制系统不同,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