Git
章节 ▾ 第二版

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命令时的文件。如果您现在提交,则上次运行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 自动添加或甚至显示为未跟踪的文件。这些通常是自动生成的文件,例如日志文件或构建系统生成的文件。在这种情况下,您可以创建一个列出匹配它们的模式的文件,命名为.gitignore。这是一个.gitignore文件示例

$ cat .gitignore
*.[oa]
*~

第一行告诉 Git 忽略以“.o”或“.a”结尾的任何文件——可能是构建代码的产物对象和存档文件。第二行告诉 Git 忽略所有名称以波浪号(~)结尾的文件,许多文本编辑器(如 Emacs)使用它来标记临时文件。您还可以包含日志、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 \*~

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

移动文件

与许多其他 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