这是阅读Pro Git内容的一些笔记。以下内容包括该书开头到2.2节的内容。

版本控制系统的一些过去

在Git出现之前,版本控制系统(Version Control System, VCS)主要有两种类型:集中式版本控制系统(Centralized VCS)和分布式版本控制系统(Distributed VCS)。但在这两种类型之前,还有一种更早期的版本控制方法,称为本地版本控制系统(Local VCS)。

本地版本控制系统

在本地版本控制系统中,版本控制是通过在本地文件系统上创建文件的备份来实现的。开发者会手动复制文件,并将其保存在不同的目录中,以表示不同的版本。这种方法虽然简单,但存在一些明显的缺点,比如难以管理多个版本、容易出错以及无法协作等。
由此,本地版本控制系统逐渐被集中式版本控制系统所取代。

集中式版本控制系统

集中式版本控制系统(如CVS、Subversion)引入了一个中央服务器,所有的代码和版本历史记录都存储在这个服务器上。开发者通过网络连接到服务器,进行代码的提交、更新和查看历史记录等操作。这种方法解决了本地版本控制系统的一些问题,比如协作和版本管理,但也带来了一些新的挑战,比如对网络连接的依赖以及单点故障的问题。如果中央服务器出现故障,所有开发者都无法访问代码库。这对大型工程来讲是一个重大的风险。
于是,分布式版本控制系统应运而生。

分布式版本控制系统

与集中式版本控制系统不同,分布式版本控制系统(如Git、Mercurial)允许每个开发者在本地拥有完整的代码库和历史记录的副本。这样,即使中央服务器出现问题,开发者们仍然可以继续工作,并在服务器恢复后同步他们的更改。这种方法不仅提高了系统的可靠性,还增强了协作的灵活性。

Git 的特性

  • 直接记录快照: 与其他基于差异的版本控制系统(比如CVS、Subversion)不同的是,Git不保存一个基本文件和一系列补丁,而是保存一系列对所有文件系统的历史快照和快照的索引。如果某个文件没有修改,那么Git不会再保存这个文件,而是保存一个指向之前相同文件的链接。
  • 本地执行: 同时,Git是分布式的,每个开发者的电脑上都有完整的代码库和历史记录,这样即使没有网络连接,也可以进行提交、查看历史等操作,也因此Git拥有非常快的速度。
  • 数据完整性: Git使用SHA-1哈希值来唯一标识每个对象(文件、目录、提交等),这样可以确保数据的完整性,防止数据被篡改。
  • 一般只追加操作: Git中的大多数操作都是追加操作,这样可以最大限度地减少数据丢失的风险。

三种状态

在Git中,文件可以处于三种状态之一:已修改(modified)、已暂存(staged)和已提交(committed)。理解这三种状态对于有效地使用Git非常重要。

  • 已修改(modified): 当你对文件进行更改后,文件处于已修改状态。这意味着文件的内容已经发生变化,但这些更改还没有被记录到Git的版本历史中。
  • 已暂存(staged): 这表示对已修改的文件做好了标记,并已经准备好被提交到下一个快照中。
  • 已提交(committed): 这代表这些数据已经被安全地存储在本地数据库中,形成了一个新的版本快照。

由此,一个典型的Git项目将包括三个区域:工作目录(working directory)、暂存区(staging area)和Git目录(Git directory)。工作目录是你实际进行文件编辑的地方,暂存区是一个中间区域,用于准备即将提交的更改,而Git目录则是Git用来存储所有版本历史记录和对象的地方。

Git三种状态示意图

基本的工作流程通常如下:

  1. 在工作目录中修改文件,文件变为已修改状态。
  2. 将想要提交的更改添加到暂存区,文件变为已暂存状态。
  3. 提交暂存区的更改到Git目录,文件变为已提交状态。

记录和更新状态

当你在Git目录中工作时,文件都只有两种状态:已跟踪(tracked)和未跟踪(untracked)。已跟踪的文件指的是那些已经被纳入版本控制的文件,也就是说上次快照中有它们的身影;未跟踪的文件则是那些新创建的文件,Git还没有将它们纳入版本控制。
当你编辑过一个已跟踪的文件后,它会变为已修改状态。如果你想将这些更改纳入下一个提交,你需要先将它们添加到暂存区,这样它们就变为已暂存状态。相反,未跟踪的文件不会自动被纳入版本控制,除非你明确地将它们添加到Git中。

Git文件状态示意图

查看和更改文件的状态

使用git status命令可以查看当前工作目录和暂存区的状态,了解哪些文件是已修改、已暂存或未跟踪的。
当你刚克隆一个Git仓库时,通常会看到如下输出:

1
2
3
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean

这表示你当前在main分支上,并且工作目录是干净的,没有任何未提交的更改。
如果你创建了一个新文件example.txt,然后运行git status,你会看到类似如下的输出:

1
2
3
4
Untracked files:
(use "git add <file>..." to include in what will be committed)
example.txt
nothing added to commit but untracked files present (use "git add" to track)

这表示example.txt是一个未跟踪的文件,Git还没有将它纳入版本控制。如果你想将它添加到版本控制中,可以使用git add example.txt命令,然后再次运行git status,你会看到:

1
2
3
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: example.txt

这表示example.txt现在已经被添加到暂存区,准备提交。
现在,假设你对example.txt进行了修改,然后再次运行git status,你会看到:

1
2
3
4
5
6
7
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: example.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: example.txt

这表示example.txt已经被添加到暂存区,但你对它进行了进一步的修改,这些修改还没有被添加到暂存区。如果你想将这些修改也纳入下一个提交,可以再次使用git add example.txt命令。
然后我们再来看另外一种情况,假设你有一个已跟踪的文件example.txt,你对它进行了修改,然后运行git status,你会看到:

1
2
3
4
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: example.txt

这表示example.txt是一个已跟踪的文件,并且它已经被修改,但这些修改还没有被添加到暂存区。如果你想将这些修改纳入下一个提交,可以使用git add example.txt命令。
看起来相当繁琐是吧?不过熟悉之后就会觉得很自然了。当然,后面我们也会谈到如何跳过暂存区,直接提交修改。

状态简览

git status命令输出的内容有时候过于详细了,当你开始熟悉Git之后,可以使用git status -s命令来获得简洁的状态输出。例如:

1
2
3
4
5
 M example.txt
MM Rakefile
A lib/git.rb
M README.md
?? new_file.txt

在这个简洁的输出中,每一行表示一个文件的状态。第一列表示文件在暂存区的状态,第二列表示文件在工作目录的状态。具体含义如下:

  • (空格):表示文件没有变化。
  • M:表示文件被修改了(Modified)。
  • A:表示文件被添加到暂存区(Added)。
  • D:表示文件被删除了(Deleted)。
  • R:表示文件被重命名了(Renamed)。
  • C:表示文件被复制了(Copied)。
  • ??:表示文件是未跟踪的(Untracked)。
  • !!:表示文件被忽略了(Ignored)。
  • U:表示文件存在冲突(Unmerged)。

在上面的例子中,example.txt在工作目录中被修改了但还没有添加到暂存区,Rakefile曾被修改后添加到暂存区,然而之后又被修改且尚未将新的修改添加到暂存区,lib/git.rb被添加到了暂存区,README.md在工作目录中被修改了但还没有添加到暂存区,而new_file.txt是一个未跟踪的文件。

忽略文件

有时候,我们不希望Git跟踪某些文件,比如编译生成的二进制文件、日志文件或临时文件等。为此,Git提供了一个名为.gitignore的文件,用于指定哪些文件或目录应该被忽略,不纳入版本控制。
你可以在项目的根目录下创建一个名为.gitignore的文件,并在其中列出要忽略的文件或目录的模式。例如:

1
2
3
4
5
6
7
cat .gitignore
# 忽略所有的 .log 文件
*.log
# 忽略.o .a文件
*.[oa]
# 忽略备份文件
*~

在上面的例子中,.gitignore文件指定了三种模式:忽略所有以.log结尾的文件,忽略所有以.o.a结尾的文件,以及忽略所有以~结尾的备份文件。在初始化Git仓库时,最好要养成设置.gitignore文件的习惯,这样可以避免不必要的文件被纳入版本控制,保持代码库的整洁。
通常来讲,.gitignore文件的格式如下:

  • 空行或以#开头的行被视为注释,它们都会被忽略。
  • 可以使用glob模式匹配,它会递归地应用于整个工作区中
  • 匹配模式可以以斜杠(/)开头以防止递归匹配。
  • 以斜杠(/)结尾的模式表示指定目录。
  • 以感叹号(!)开头的模式表示取消忽略某些文件或目录。

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。 星号(*)匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符 (这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c); 问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符, 表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。 使用两个星号(**)表示匹配任意中间目录,比如 a/**/z 可以匹配 a/z 、 a/b/z 或 a/b/c/z 等。

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

如果git status的输出对你来说太过于笼统了,你可以使用git diff命令来查看具体的更改内容。默认情况下,git diff显示的是工作目录和暂存区之间的差异,也就是未暂存的更改。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ 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
WIP). This helps avoid duplicated work and allows others to give early
feedback.

在上面的输出中,git diff显示了CONTRIBUTING.md文件的具体更改内容。以-开头的行表示被删除的内容,以+开头的行表示新增的内容。
如果你想查看已经暂存但还没有提交的更改,可以使用git diff --staged命令(或者git diff --cached,两者是等价的)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git diff --staged
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 643e24f..b1f3e5a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -70,6 +70,7 @@ in the first place, you're less likely to get feedback and have your change
merged in. Also, split your changes into comprehensive chunks if your patch is
longer than a dozen lines.
+Additional line added.
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
WIP). This helps avoid duplicated work and allows others to give early
feedback.

但是这里就无法显示未暂存的更改了。

提交更新

现在,暂存区已经准备就绪,可以提交了。但在此之前,你最好先使用git status命令确认一下当前的状态,确保所有想要提交的更改都已经被添加到暂存区。
一旦确认无误,可以使用git commit命令来提交更改。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 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打开了一个文本编辑器,让你输入提交信息。你可以在编辑器中输入一条简短而有意义的提交信息,描述这次提交所包含的更改内容。保存并关闭编辑器后,Git会完成提交操作,并将更改记录到版本历史中。
你也可以使用-m选项直接在命令行中提供提交信息,例如:

1
git commit -m "Add README and update CONTRIBUTING.md"

这条命令会将暂存区的更改提交到版本历史中,并附加一条简短的提交信息。

跳过使用暂存区

有时候,你可能想要跳过暂存区,直接将工作目录中的更改提交到版本历史中。为此,你可以使用-a选项与git commit命令结合使用。例如:

1
git commit -a -m "Update CONTRIBUTING.md"

这条命令会自动将所有已跟踪的文件的更改添加到暂存区,并立即提交这些更改到版本历史中。需要注意的是,使用-a选项只会影响已跟踪的文件,未跟踪的文件仍然需要使用git add命令手动添加到暂存区。

移除文件

如果你想要从Git仓库中移除一个文件,你需要先将它从工作目录中删除,然后使用git rm命令将其从Git的版本控制中移除。例如:

1
2
3
rm example.txt
git rm example.txt
git commit -m "Remove example.txt"

假如你只是手动地将文件删除,而不使用git rm命令将它从版本控制中移除,那么当前文件的状态将会显示为已删除(deleted),但它仍然存在于Git的版本历史中(即git staus将显示changes not staged for commitment)。
比如,你删除了一个尚未提交的已跟踪的文件example.txt,然后运行git status,你会看到:

1
2
3
4
5
6
7
8
9
10
11
touch example.txt
git add example.txt
rm example.txt
git status
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: example.txt
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: example.txt

你需要使用git rm命令来告诉Git这个文件已经被删除并在暂存区中删除这个文件的记录,然后再进行提交。比如:

1
2
3
4
5
6
git rm example.txt
rm 'example.txt'
git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean

如果该文件之前是未跟踪的文件,直接删除即可,无需使用git rm命令。但是如果要删除之前修改过或者已经放到暂存区的文件,必须使用强制删除选项,即使用git rm -f <file>命令。使用这一命令的后果是,该文件会从工作目录和Git的版本控制中同时被删除。因此必须谨慎使用这个命令,此命令删除的文件将无法由Git恢复。
但是,如果你只是想将文件从版本控制中移除(也就是从暂存区中移除),但仍然希望保留该文件在工作目录中,可以使用--cached选项。例如:

1
2
git rm --cached example.txt
git commit -m "Remove example.txt from version control but keep it in working directory"

这条命令会将example.txt从Git的版本控制中移除,但文件仍然保留在工作目录中。我们也可使用glob模式来批量移除文件,比如:

1
2
git rm --cached *.log
git commit -m "Remove all .log files from version control"

这些命令会将所有以.log结尾的文件从Git的版本控制中移除(也就是Git仓库中移除),但这些文件仍然保留在工作目录中。

移动文件

如果你想要移动或重命名一个文件,最好使用git mv命令,而不是直接使用操作系统的移动命令。git mv命令会同时更新Git的版本控制信息,确保文件的历史记录得以保留。例如:

1
2
git mv old_filename.txt new_filename.txt
git commit -m "Rename old_filename.txt to new_filename.txt"

实际上,git mv命令相当于执行了以下三个步骤:

1
2
3
mv old_filename.txt new_filename.txt
git rm old_filename.txt
git add new_filename.txt

无论采取哪种方式,Git都会识别出文件的重命名操作,并在提交历史中保留文件的变更记录。

小结

  • Git是一种分布式版本控制系统,允许每个开发者在本地拥有完整的代码库和历史记录的副本。
  • Git中的文件可以处于已修改、已暂存和已提交三种状态。
  • 使用git status命令可以查看当前工作目录和暂存区的状态。
  • 使用.gitignore文件可以指定哪些文件或目录应该被忽略,不纳入版本控制。
  • 使用git diff命令可以查看具体的更改内容。
  • 使用git add命令可以将文件添加到暂存区。
  • 使用git commit命令可以将暂存区的更改提交到版本历史中。
  • 使用git commit -a命令可以跳过暂存区,直接提交已跟踪文件的更改。
  • 使用git commit -m "message"命令可以在命令行中直接提供提交信息。
  • 使用git rm命令可以从Git的版本控制中移除文件。
  • 使用git rm --cached命令可以将文件从版本控制中移除,但保留在工作目录中。
  • 使用git mv命令可以移动或重命名文件,同时更新Git的版本控制信息。

2026年1月17日。