附录 D:使用 Git 进行版本控制

版本控制软件允许你在项目处于工作状态时拍摄快照。当你对项目进行修改时—​例如,当你实施一项新功能时—​如果项目的当前状态运行不佳,你可以返回到之前的工作状态。

使用版本控制软件可以让您自由地进行改进和犯错,而不必担心毁掉您的项目。这一点在大型项目中尤为重要,但在小型项目中也很有帮助,甚至当你在处理单个文件中的程序时也是如此。

在本附录中,你将学习如何安装 Git 并用它来控制你正在运行的程序的版本。Git 是当今最流行的版本控制软件。

它的许多高级工具都能帮助团队协作完成大型项目,但其最基本的功能也同样适用于个人开发者。Git 通过跟踪项目中每个文件的改动来实现版本控制;如果你犯了错误,只需返回之前保存的状态即可。

安装Git

Git 可在所有操作系统上运行,但在每个系统上的安装方法各不相同。下文将针对每种操作系统提供具体说明。

Git 在某些系统中是默认安装的,通常会与其他已安装的软件包捆绑在一起。在尝试安装 Git 之前,先看看你的系统上是否已经安装了它。打开一个新的终端窗口,执行 git --version 命令。如果输出结果列出了具体的版本号,说明系统上已经安装了 Git。如果看到提示安装或更新 Git 的信息,请按照屏幕上的说明操作。

如果你使用的是 Windows 或 macOS,看不到屏幕提示,可以从 https://git-scm.com 下载安装程序。如果你使用的是与 apt 兼容的 Linux 系统,可以使用 sudo apt install git 命令安装 Git。

配置 Git

Git 会记录谁修改了项目,即使只有一个人在做项目。为此,Git 需要知道你的用户名和电子邮件地址。你必须提供用户名,但也可以伪造一个电子邮件地址:

$ git config --global user.name "username"
$ git config --global user.email "username@example.com"

如果忘记了这一步,Git 会在你第一次提交时提示你提供这些信息。

最好还能为每个项目的主分支设置默认名称。主分支的好名字是 main:

$ git config --global init.defaultBranch main

这种配置意味着,你用 Git 管理的每个新项目一开始都会有一个名为 main 的提交分支。

创建项目

让我们来做一个项目。在系统中创建一个名为 git_practice 的文件夹。在文件夹中,创建一个简单的 Python 程序:

hello_git.py
print("Hello Git world!")

我们将用这个程序来探索 Git 的基本功能。

忽略文件

扩展名为 .pyc 的文件是由 .py 文件自动生成的,因此我们不需要 Git 来跟踪它们。这些文件保存在一个名为 __pycache__ 的目录中。要让 Git 忽略这个目录,可以创建一个名为 .gitignore 的特殊文件(文件名开头有一个点,没有文件扩展名),并在其中添加以下一行:

.gitignore
__pycache__/

该文件告诉 Git 忽略 __pycache__ 目录中的任何文件。使用 .gitignore 文件能让你的项目保持整洁,更易于操作。

你可能需要修改文件浏览器的设置,以便显示隐藏文件(文件名以点开头的文件)。在 Windows 资源管理器中,选中 "查看 "菜单中标有 "隐藏项目 "的复选框。在 macOS 上,按 ⌘-SHIFT-. (点)。在 Linux 中,查找标有 "显示隐藏文件" 的设置。

如果你使用的是 macOS,请在 .gitignore 中多加一行。添加 .DS_Store 名称;这些是隐藏文件,包含 macOS 上每个目录的信息,如果不添加到 .gitignore 中,它们会扰乱你的项目。

初始化存储库

有了包含 Python 文件和 .gitignore 文件的目录,就可以初始化 Git 仓库了。打开终端,导航到 git_practice 文件夹,运行以下命令:

git_practice$ git init
Initialized empty Git repository in git_practice/.git/
git_practice$

输出结果显示,Git 在 git_practice 中初始化了一个空仓库。所谓仓库,就是 Git 主动跟踪的程序中的文件集。Git 用来管理仓库的所有文件都位于隐藏目录 .git,你根本不需要使用它。只要不删除该目录,就不会丢失项目历史。

检查状态

在做其他事情之前,我们先来看看项目的现状:

git_practice$ git status
❶ On branch main
No commits yet

❷ Untracked files:
(use "git add <file>..." to include in what will be committ
ed)
    .gitignore
    hello_git.py

❸ nothing added to commit but untracked files present (use "git add" to track)
git_practice$

在 Git 中,分支是正在运行的项目的一个版本;这里可以看到我们所在的分支名为 main ❶。每次查看项目状态时,都会显示你在 main 分支上。然后,你会看到我们即将进行初始提交。提交是项目在某个特定时间点的快照。

Git 会告诉我们项目❷中有未跟踪的文件,因为我们还没告诉它要跟踪哪些文件。然后我们会被告知,当前提交中没有添加任何内容,但有我们可能想添加到版本库❸ 的未跟踪文件。

将文件添加到存储库

让我们将这两个文件添加到存储库并再次检查状态:

❶ git_practice$ git add .
❷ git_practice$ git status
   On branch main
   No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
❸     new file: .gitignore
      new file: hello_git.py

git_practice$

git add 命令会将项目中所有尚未被跟踪❶ 的文件添加到版本库中,只要这些文件没有被列入 .gitignore 中。它不会提交这些文件,只是告诉 Git 开始关注它们。现在查看项目状态,我们可以看到 Git 发现了一些需要提交的改动❷。标签 new file 表示这些文件是新添加到版本库的❸。

提交

让我们进行第一次提交:

❶ git_practice$ git commit -m "Started project."
❷ [main (root-commit) cea13dd] Started project.
❸ 2 files changed, 5 insertions(+)
create mode 100644 .gitignore
create mode 100644 hello_git.py
❹ git_practice$ git status
On branch main
nothing to commit, working tree clean
git_practice$

我们发出 git commit -m "message" ❶ 命令来制作项目快照。-m标志会告诉 Git 在项目日志中记录后面的信息(Started project.)。输出结果显示我们在主分支 ❷,有两个文件发生了变化 ❸。

现在检查状态,可以看到我们在主分支上,并且有一个干净的工作树 ❹。每次提交项目的工作状态时,都应该看到这条信息。如果收到的是不同的信息,请仔细阅读;这很可能是你在提交前忘记添加文件了。

检查日志

Git 保留对项目所做的所有提交的日志。我们来检查一下日志:

git_practice$ git log
commit cea13ddc51b885d05a410201a54faf20e0d2e246 (HEAD -> main)
Author: eric <eric@example.com>
Date: Mon Jun 6 19:37:26 2022 -0800
Started project.
git_practice$

每次提交时,Git 都会生成一个唯一的、40 个字符的引用 ID。它还会记录提交者、提交时间和记录的信息。你并不总是需要这些信息,所以 Git 提供了一个选项来打印更简单的日志条目:

git_practice$ git log --pretty=oneline
cea13ddc51b885d05a410201a54faf20e0d2e246 (HEAD -> main) Started project.
git_practice$

--pretty=oneline 标志提供了两个最重要的信息:提交的引用 ID 和为提交记录的消息。

第二次提交

要了解版本控制的真正威力,我们需要对项目进行修改并提交该修改。在这里,我们只需在 hello_git.py 中添加一行即可:

hello_git.py
print("Hello Git world!")
print("Hello everyone.")

在查看项目状态时,我们会发现 Git 已经注意到文件发生了变化:

git_practice$ git status
❶ On branch main
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: hello_git.py
❸ no changes added to commit (use "git add" and/or "git commit -a")
git_practice$

我们可以看到正在运行的分支 ❶、被修改的文件名 ❷,以及尚未提交的更改 ❸。让我们提交修改并再次检查状态:

❶ git_practice$ git commit -am "Extended greeting."
[main 945fa13] Extended greeting.
1 file changed, 1 insertion(+), 1 deletion(-)
❷ git_practice$ git status
On branch main
nothing to commit, working tree clean
❸ git_practice$ git log --pretty=oneline
945fa13af128a266d0114eebb7a3276f7d58ecd2 (HEAD -> main) Exten
ded greeting.
cea13ddc51b885d05a410201a54faf20e0d2e246 Started project.
git_practice$

在使用 git commit ❶ 命令时,我们会通过 -am 标志来提交一个新版本。-a标志会告诉 Git 将版本库中所有修改过的文件添加到当前提交中。(如果你在两次提交之间创建了新文件,请重新执行 git add .命令将新文件加入版本库)。-m标志会告诉 Git 在日志中记录本次提交的信息。

当我们查看项目状态时,会发现我们又有了一个干净的工作树❷。最后,我们会在日志❸ 中看到两条提交信息。

放弃变更

现在,让我们看看如何放弃更改,回到之前的工作状态。首先,在 hello_git.py 中添加一行新内容:

hello_git.py
print("Hello Git world!")
print("Hello everyone.")

print("Oh no, I broke the project!")

保存并运行该文件。

我们检查状态并看到 Git 注意到了这一变化:

git_practice$ git status
On branch main
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: hello_git.py
no changes added to commit (use "git add" and/or "git commit -a")
git_practice$

Git 发现我们修改了 hello_git.py ❶,于是我们就可以提交了。但这次,我们不会提交修改,而是回到上次提交时,我们知道我们的项目正在运行。我们不会对 hello_git.py 做任何修改:我们不会删除该行,也不会使用文本编辑器中的撤消功能。相反,在终端会话中输入以下命令:

git_practice$ git restore .
git_practice$ git status
On branch main
nothing to commit, working tree clean
git_practice$

git restore filename 命令允许您放弃特定文件中自上次提交以来的所有改动。命令 git restore . 则会放弃上次提交后所有文件中的所有改动;该操作会将项目恢复到上次提交时的状态。

返回文本编辑器后,hello_git.py 就会恢复成这样:

print("Hello Git world!")
print("Hello everyone.")

虽然在这个简单的项目中,回到之前的状态似乎微不足道,但如果我们正在处理的是一个拥有数十个修改文件的大型项目,那么自上次提交后发生变化的所有文件都将被还原。这个功能非常有用:在实现新功能时,你可以随意修改,如果这些修改不起作用,你可以放弃它们,而不会影响项目。你不必记住这些改动,也不必手动撤销它们。Git 可以帮你做到这一切。

您可能需要在编辑器中刷新文件,才能看到恢复后的版本。

检查以前的提交

您可以使用签出命令,通过引用 ID 的前六个字符重新查看日志中的任何提交。签出并查看早期提交后,您可以返回最新提交,或放弃最近的工作,重新开始早期提交的开发:

git_practice$ git log --pretty=oneline
945fa13af128a266d0114eebb7a3276f7d58ecd2 (HEAD -> main) Extended greeting.
cea13ddc51b885d05a410201a54faf20e0d2e246 Started project.
git_practice$ git checkout cea13d
Note: switching to 'cea13d'.
❶ You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

❷ Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at cea13d Started project.
git_practice$

当你签出之前的提交时,你就离开了主分支,进入了 Git 所说的脱离 HEAD 状态❶。HEAD 是项目当前已提交的状态;脱离 HEAD 是因为你离开了一个指定的分支(本例中是主分支)。

要回到主分支,你可以按照建议❷ 撤销之前的操作:

git_practice$ git switch -
Previous HEAD position was cea13d Started project.
Switched to branch 'main'
git_practice$

这条命令会将你带回主分支。除非你想使用 Git 更高级的功能,否则最好不要在签出之前的提交时对项目做任何改动。不过,如果只有你一个人在处理项目,而你又想放弃所有最近的提交,回到之前的状态,你可以将项目重置为之前的提交。从主分支开始,输入以下内容:

❶ git_practice$ git status
On branch main
nothing to commit, working directory clean
❷ git_practice$ git log --pretty=oneline
945fa13af128a266d0114eebb7a3276f7d58ecd2 (HEAD -> main) Extended greeting.
cea13ddc51b885d05a410201a54faf20e0d2e246 Started project.
❸ git_practice$ git reset --hard cea13d
HEAD is now at cea13dd Started project.
❹ git_practice$ git status
On branch main
nothing to commit, working directory clean
❺ git_practice$ git log --pretty=oneline
cea13ddc51b885d05a410201a54faf20e0d2e246 (HEAD -> main) Started project.
git_practice$

我们首先检查状态,确保我们在主分支 ❶。在日志中,我们看到了两个提交❷。然后,我们发出 git reset --hard 命令,并在其中输入我们要永久返回的提交的引用 ID 的前六个字符❸。我们再次查看状态,发现我们在主分支上,没有任何要提交的内容❹。再次查看日志时,我们会发现我们正处于想要重新开始的提交❺。

删除存储库

有时你会弄乱版本库的历史记录,不知道如何恢复。如果发生这种情况,首先考虑使用附录 C 中讨论的方法寻求帮助。如果无法修复,而你又在单独开发一个项目,你可以继续处理文件,但要删除 .git 目录,以清除项目的历史记录。这不会影响任何文件的当前状态,但会删除所有提交,因此您将无法查看项目的任何其他状态。

为此,您可以打开文件浏览器并删除 .git 仓库,或者从命令行中删除它。之后,您需要从一个全新的版本库重新开始,重新跟踪您的修改。下面是整个过程在终端会话中的样子:

❶ git_practice$ git status
  On branch main
  nothing to commit, working directory clean
❷ git_practice$ rm -rf .git/
❸ git_practice$ git status
fatal: Not a git repository (or any of the parent directories): .git
❹ git_practice$ git init
Initialized empty Git repository in git_practice/.git/
❺ git_practice$ git status
  On branch main
  No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
    .gitignore
    hello_git.py

nothing added to commit but untracked files present (use "git add" to track)
❻ git_practice$ git add .
git_practice$ git commit -m "Starting over."
[main (root-commit) 14ed9db] Starting over.
2 files changed, 5 insertions(+)
create mode 100644 .gitignore
create mode 100644 hello_git.py
❼ git_practice$ git status
On branch main
nothing to commit, working tree clean
git_practice$

我们首先检查状态,看看是否有一个干净的工作目录❶。然后使用 rm -rf .git/ 命令删除 .git 目录(Windows 下为 del .git)❷。删除 .git 文件夹后,当我们查看状态时,会被告知这不是一个 Git 仓库 ❸。Git 用于跟踪仓库的所有信息都存储在 .git 文件夹中,因此删除它就等于删除了整个仓库。

这样我们就可以使用 git init 来启动一个新的版本库❹。检查状态显示我们回到了初始阶段,等待第一次提交❺。我们添加了文件,并进行了第一次提交❻。现在查看状态显示,我们在新的主分支上,没有任何要提交的内容❼。

使用版本控制需要一些练习,但一旦开始使用,你就再也不想离开它了。