章节 ▾ 第二版

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 这样做之前,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 自动添加,甚至不想让你看到它们是未跟踪的。 这些通常是自动生成的文件,例如日志文件或构建系统生成的文件。 在这种情况下,你可以创建一个文件,其中列出匹配这些文件的模式,该文件名为 .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 维护了一个相当全面的好的 .gitignore 文件示例列表,适用于数十个项目和语言,网址为 https://github.com/github/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 命令的最新输出,这些输出被注释掉了,顶部有一个空行。 你可以删除这些注释并键入你的提交消息,或者你可以将它们留在那里以帮助你记住你要提交的内容。

注意

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

当你退出编辑器时,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 提供了一个简单的快捷方式。 将 -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(-)

请注意,在这种情况下,你在提交之前不必在 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

你可以将文件、目录和文件全局模式传递给 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