一、理论基础
1.1 Git 记录的是什么?
Git 和其它版本控制系统(如 SVN)的主要差别在于 Git 对待数据的方式。
1.1.1 SVN 记录差异比较
从概念上来说,SVN 以文件变更列表的方式存储信息,这类系统将它们存储的信息看作是一组基本文件和每个文件随时间逐步累积的差异 ,它们通常称作 基于差异(delta-based) 的版本控制,如下图所示:
- 每一次版本迭代,SVN 记录的是文件的变化内容。
通常让我们自己来写一个版本管理工具也会首选这样的思维吧。就像写小说一样,每次就新增一个章节,修改若干错别字,最终装订成册,没必要为每次的修改都拷贝一整本书!
这种存储方式也是有名堂的,叫增量文件系统(Delta Storage systems)。
1.1.2 Git 记录文件快照
Git 不按照上述方式对待或保存数据,反之,Git 更像是把数据看作是对小型文件系统的一系列快照。
在 Git 中,每当你提交更新或保存项目状态时,它基本上就会对当时的全部文件创建一个快照并保存这个快照的索引。 为了效率,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。
- 如果每个版本中有文件发生变动,Git 会将整个文件复制并保存起来。
Git 对待数据更像是一个快照流,这种设计看似会多消耗更多的空间,但在分支管理时却是带来了很多的益处和便利(分支管理我们后边会讲,不急)。
1.2 近乎所有操作都是本地执行
在 Git 中的绝大多数操作都只需要访问本地文件和资源,一般不需要来自网络上其它计算机的信息。 如果你习惯于所有操作都有网络延时开销的集中式版本控制系统,那 Git 在这方面会让你感到速度之神速。 因为你在本地磁盘上就有项目的完整历史,所以大部分操作看起来瞬间完成。
举个例子,要浏览项目的历史,Git 不需要外连到服务器去获取历史,然后再显示出来,你能立即看到项目历史(因为它只需直接从本地数据库中读取)。如果你想查看当前版本与一个月前的版本的差异, Git 会查找到一个月前的文件做一次本地的差异计算,而不是由远程服务器处理或从远程服务器拉回旧版本文件再来本地处理。这也意味着你在没有网络时,几乎可以进行任何操作。 如你在飞机或火车上想做些工作,就能愉快地提交到你的本地,直到有网络连接时再上传。
但如果是使用其它系统的话,做到这些是不可能的或很费力。比如,用 SVN,你能修改文件,但不能向数据库提交修改(因为你断网了)。 这样似乎问题不大,但是你可能会惊喜地发现它带来的巨大的不同。
1.3 Git 文件分类
Git 将所有文件分为三类:已追踪的、被忽略的和未追踪的。
1.3.1 已跟踪的(Tracked)
已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录。在工作一段时间后, 它们的状态可能是未修改(unmodify),已修改(modified)或已放入暂存区(staged)。简而言之,已跟踪的文件就是 Git 已经知道的文件。
1.3.2 被忽略的(Ignored)
一般我们总会有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以创建一个名为 .gitignore
的文件,列出要忽略的文件的模式,来看一个实际的例子:
*.[oa]
*~
- 第一行告诉 Git 忽略所有以 .o 或 .a 结尾的文件。一般这类对象文件和存档文件都是编译过程中出现的。
- 第二行告诉 Git 忽略所有名字以波浪符(~)结尾的文件,许多文本编辑软件(如 Emacs)都用这样的文件名保存副本。
此外,你可能还需要忽略 log、tmp 或者 pid 目录,以及自动生成的文档等等。要养成一开始就为你的新仓库设置好 .gitignore 文件的习惯,以免将来误提交这类无用的文件。
文件 .gitignore
的格式规范如下:
- 所有空行或者以
#
开头的行都会被 Git 忽略。 - 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
- 匹配模式可以以(
/
)开头防止递归。 - 匹配模式可以以(
/
)结尾指定目录。 - 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(
!
)取反。
所谓的 glob 模式是指 shell 所使用的简化了的正则表达式:
- 星号(
*
)匹配零个或多个任意字符; [abc]
匹配任何一个列在方括号中的字符 (这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);- 问号(
?
)只匹配一个任意字符; - 如果在方括号中使用短划线分隔两个字符, 表示所有在这两个字符范围内的都可以匹配(比如
[0-9]
表示匹配所有 0 到 9 的数字) - 使用两个星号表示匹配任意中间目录
我们再看一个 .gitignore
文件的例子:
# 忽略所有的 .a 文件
*.a
# 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件
!lib.a
# 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO
/TODO
# 忽略任何目录下名为 build 的文件夹
build/
# 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt
doc/*.txt
# 忽略 doc/ 目录及其所有子目录下的 .pdf 文件
doc/**/*.pdf
1.3.3 未跟踪的(Untracked)
未追踪的文件是指那些不在前两类中的文件,这类文件既不存在于上次快照的记录中,也没有被放入暂存区。
1.4 Git 三区
相应的,Git 拥有三个工作区域:
- 工作区(Workspace)
- 暂存区(Stage/Index)
- 仓库区(Repository 或 Git Directory)
如果在加上远程的 Git 仓库(Remote Directory),就可以分为四个工作区域,这是 Git 的核心框架。
文件在这四个区域之间的转换关系如下图所示:
- 工作区(也叫工作目录):就是你平时存放项目代码的地方。
- 暂存区:用于临时存放你的改动。事实上它只是一个文件,保存了下次将要提交的文件列表信息,一般存放在 .git 目录下的 index 文件中,所以我们把暂存区有时也叫作索引(index),一般在 Git 仓库目录中。
- 仓库区(或本地仓库):就是安全存放数据的位置。这里面有你提交到所有版本的数据,这是 Git 中最重要的部分,从其它计算机克隆仓库时,复制的就是这里的数据。
- 远程仓库:托管代码的服务器,如 GitHub、Gitee。
二、准备工作
2.1 git config 命令介绍
2.1.1 基本语法
Git 自带一个 git config 的工具来帮助用户获取和设置配置变量。
$ git config --配置文件范围 user.name 用户名
$ git config --配置文件范围 user.email 邮箱
配置文件范围有三个取值:
- local(本地配置):是对单个 git 仓库的配置,仅在当前本地仓库有效
- system( 系统级别的配置):对应的是所有操作系统的用户
- golbal (全局配置):对应的是单个系统用户对所有 git 仓库的配置,即系统用户级别,全局配置(登录当前操作系统
的用户范围)
2.1.2 说明
根据配置文件范围取值的不同,分别对如下三个文件生效:
- .git/config(当前使用仓库目录中的 config 文件):针对该仓库,你可以通过使用
--local
选项让 Git 强制读写此文件(当然,你需要进入某个 Git 仓库中才能让该选项生效)。 - /etc/gitconfig(Windows 中对应的路径为 Git 的安装目录):该文件包含系统上每一个用户及他们仓库的通用配置,使用
--system
选项,让 Git 读写该文件中的配置变量(由于它是系统配置文件,因此你需要管理员或超级用户权限来修改它)。 - ~/.gitconfig(Windows 中对应的路径为 /c/Users/${user}/):该文件只针对当前用户,通过使用
--global
选项让 Git 读写此文件,这会对你系统上所有的仓库生效。
每一个级别会覆盖上一级别的配置,所以 .git/config 的配置变量会覆盖 .gitconfig 中的配置变量。
你可以通过以下命令查看所有的配置以及它们所在的文件:
$ git config --list --show-origin
2.2 设置用户签名
2.2.1 设置用户签名的作用
安装完 Git 之后,要做的第一件事就是设置你的用户名和邮件地址。设置用户签名的含义就是说当前仓库是谁在操作,用户的签名信息在每一个版本的提交信息中能够看到,以此确认本次提交是谁做的。
2.2.2 设置全局用户签名
在实际开发中,我们是不会给每一个仓库设置一个本地用户的,又因为全局用户可以在所有的仓库中都看到,所以我们直接设置一个全局用户来操作所有的仓库即可,设置全局用户签名可以使用如下指令:
$ git config --global user.name 用户名
$ git config --global user.email 邮箱
$ git config --global -l # 查看全局用户签名
如果使用了 --global
选项,那么该命令只需要运行一次,因为之后无论你在该系统上做任何事情, Git 都会使用全局的用户信息。 当你想针对特定项目使用不同的用户名和邮箱时,可以在那个项目目录下使用没有 --global
选项的命令来配置。
Notes:
- Git 首次安装必须设置用户签名,否则无法提交代码。
- 签名的作用是区分不同操作者身份,所以随便给自己设置一个用户名称和邮箱即可。
- 这里设置的用户签名和将来登录 GitHub(或其他代码托管中心)的账号没有任何关系。
2.3 使用 Notepad++ 作为 Git 的文本编辑器
既然用户信息已经设置完毕,你可以配置默认文本编辑器了,当 Git 需要你输入信息时会调用它。
$ git config --global core.editor "'D:\Program Files (x86)\Notepad++\notepad++.exe' -multiInst -notabbar -nosession -noPlugin"
- notepad++ 路径填写你实际的安装路径即可
或者你也可以通过如下指令使用系统默认的记事本作为 Git 的文本编辑器:
$ git config --global core.editor notepad
三、开启 Git 之旅
当你看到这儿的时候,说明你已经对 Git 的工作原理有了初步的了解,并且配置好的用户签名,下面让我们正式开启我们的 Git 之旅~
3.1 创建版本库
版本库又名仓库,英文名 Repository,你可以简单理解成一个目录,这个目录里面的所有文件的修改、删除,Git 都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以还原。
所以,这永远是 Git 操作的第一步!
首先让我们新建一个空文件夹,并命名为 gitworkspace,在该文件夹打开 Git Bush,并执行 $ git init
命令,通过该命令,我们可以把本地目录 gitworkspace变成 Git 可以管理的仓库。
$ git init
Initialized empty Git repository in E:/gitworkspace/.git/
- 使用指令的瞬间,Git 就把仓库建好了,而且告诉你是一个空的仓库(empty Git repository)。
细心的读者可以发现当前目录下多了一个 .git 的目录,这个目录就是 Git 用来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把 Git 仓库给破坏了。
Notes:如果你使用Windows系统,为了避免遇到各种莫名其妙的问题,请确保目录名(包括父目录)不包含中文。
3.2 把文件添加到版本库
首先这里再明确一下,所有的版本控制系统,其实只能跟踪文本文件的改动,比如 txt 文件、html、程序代码等等,Git 也不例外。版本控制系统可以告诉你每次的改动,比如在第 5 行加了一个单词 Linux,在第 8 行删了一个单词 Windows。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从 100KB 改成了 120KB,但到底改了啥,版本控制系统不知道,也没法知道。
不幸的是,Microsoft 的 Word 格式是二进制格式,因此,版本控制系统是没法跟踪 Word 文件的改动的。如果要真正使用版本控制系统,就要以纯文本方式编写文件。
因为文本是有编码的,比如中文有常用的 GBK 编码,日文有 Shift_JIS 编码,如果没有历史遗留问题,强烈建议使用标准的UTF-8 编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。
言归正传,现在我们新建一个 README.md 文件,并添加内容 "read me":
$ echo "read me" > README.md
将 README.md 添加到 Git 仓库只需要两步:
- 使用
$ git add README.md
命令将 README 文件由工作区添加到暂存区。 - 使用
$ git commit -m "提交 README.md"
将 README 由暂存区提交到 Git 仓库。
$ git add README.md
$ git commit -m "第一次提交 README.md"
[master (root-commit) f065b5a] 第一次提交 README.md
1 file changed, 1 insertion(+)
create mode 100644 README.md
git commit 命令执行成功后会告诉你:
- 1 file changed:1个文件被改动(我们新添加的 test.txt 文件)
- 1 insertions:插入了一行内容(test.txt 有一行内容)
3.3 查看文件状态
可以用 $ git status
命令查看文件处于什么状态,如果你看到类似这样的输出:
$ git status
On branch master
nothing to commit, working tree clean
这说明你现在的工作目录相当干净,换句话说,所有已跟踪的文件在上次提交后都未被更改过。此外,上面的信息还表明,当前目录下没有出现任何处于未跟踪状态的新文件,否则 Git 会在这里列出来。
3.3.1 未跟踪文件状态
我们创建一个新的文件 test.txt,并查看其状态:
$ echo "test" > test.txt
$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
test.txt
nothing added to commit but untracked files present (use "git add" to track)
- 在状态报告中可以看到新建的 test.txt 文件出现在 Untracked files 下面。
未跟踪的文件意味着 Git 在之前的快照(提交)中没有这些文件;Git 不会自动将之纳入跟踪范围,除非你明明白白地告诉它「我需要跟踪该文件」。 这样的处理让你不必担心将生成的二进制文件或其它不想被跟踪的文件包含进来。
不过现在的例子中,我们确实想要跟踪管理 test.txt 这个文件。
3.3.2 跟踪新文件
使用 $ git add
命令开始跟踪一个文件,并通过 $ git status
查看文件状态:
$ git add test.txt
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: test.txt
- 文件 test.txt 出现在了 Changes to be committed 下面,就说明是已暂存状态
3.3.3 暂存已修改的文件
现在,让我们来修改 README.md 这个已追踪的文件,并查看状态:
$ echo "read me" >> README.md
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: test.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: README.md
- 文件 README.md 出现在了 Changes not staged for commit 下面,说明已跟踪文件的内容发生了变化,但还没有放到暂存区,要暂存这次更新,需要运行 git add 命令。
Notes:
git add 是个多功能命令:可以用它跟踪新文件,或者把已跟踪的文件放到暂存区,此外,它还能用于合并时把有冲突的文件标记为已解决状态等。
将这个命令理解为「精确地将内容添加到下一次提交中」而不是「将一个文件添加到项目中」要更加合适。
现在让我们运行 git add 命令将 README.md 的修改添加到到暂存区,然后再看看 git status 的输出:
$ git add README.md
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.md
new file: test.txt
- 现在两个文件都已暂存,下次提交时就会一并记录到仓库。
假设此时,你想要在 README.md 里再增加点内容,重新编辑保存后,准备提交。 不过且慢,再运行 git status 看看:
$ echo "read me" >> README.md
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.md
new file: test.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: README.md
怎么回事? 现在 README.md 文件同时出现在暂存区和非暂存区。实际上,Git 只不过暂存了你运行 git add 命令时的版本。
如果你现在 commit,README.md 的版本是你最后一次运行 git add 命令时的那个版本,而不是你提交时,在工作目录中的当前版本。 所以,使用了 git add 之后又作了修订的文件,需要重新运行 git add 把最新版本重新暂存起来。
$ git add README.md
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.md
new file: test.txt
$ git commit -m "第二次提交:修改 README.md,新增 test.txt"
[master 572388a] 第二次提交:修改 README.md,新增 test.txt
2 files changed, 3 insertions(+)
create mode 100644 test.txt
当我们提交我们所做的修改后,再次运行 git status 查看工作目录状态:
$ git status
On branch master
nothing to commit, working tree clean
十分干净~
3.4 查看日志信息
在提交了若干更新,又或者克隆了某个项目之后,你也许想回顾下提交历史。 完成这个任务最简单而又有效的工具是 $ git log
命令。
接着上面的提交,我们运行 git log 指令:
$ git log
commit 572388ac59b01f06091e03a61310c499b6223401 (HEAD -> master)
Author: MElephant <[email protected]>
Date: Sun Jun 11 15:27:55 2023 +0800
第二次提交:修改 README.md,新增 test.txt
commit 563b1251c20adeda7db9899281089d24bfe1c01e
Author: MElephant <[email protected]>
Date: Sun Jun 11 15:03:27 2023 +0800
第一次提交 README.md
- 不传入任何参数的默认情况下,git log 会按时间先后顺序列出所有的提交,最近的更新排在最上面。 正如你所看到的,这个命令会列出:
- 每个提交的 SHA-1 校验和
- 作者的名字和电子邮件地址
- 提交时间
- 提交说明
你看到的一大串类似 57238... 这种的字符串,是 commitID(也就是版本号)。和 SVN 不同的是,Git 的管理的版本号不是 1、2、3 这样递增的数字,而是一个通过 SHA1 计算出来的一个非常大的数字,用 40 个十六进制表示。
SHA(Secure Hash Algorithm)的意思是安全哈希算法。SHA-1 将文件中的内容通过 hash 算法生成一个 160bit 的报文摘要(字符串),即 40 个十六进制数字。
3.5 查看文件差异
查看、比较被 Git 管理的文件在「工作区、暂存区和版本库」之间的差异,就需要用到 $ git diff
命令。
下面讨论以下四个情景下文件差异的比较:
- 工作区和暂存区
- 工作区和版本库
- 暂存区和版本库
- 不同版本之间
3.5.1 查看工作区和暂存区之间文件的差异
git diff 命令默认查看的就是「工作区和暂存区」之间的文件差异。
$ git diff # 查看工作区和暂存区之间所有文件的差异
$ git diff -- file # 查看具体某个文件在工作区和暂存区之间的差异
$ git diff -- file1 file2 # 查看多个文件在工作区和暂存区之间的差异
Notes:查看具体文件的时候,
--
和文件名之间有一个空格。
3.5.2 查看工作区和版本库之间文件的差异
$ git diff HEAD # 查看工作区与最新版本库之间的所有的文件差异
$ git diff HEAD -- file # 查看工作区与最新版本库之间的指定文件的文件差异
$ git diff commitID # 查看工作区与具体某个提交版本之间的所有的文件差异
$ git diff commitID -- file # 查看工作区与具体某个版本之间的指定文件的文件差异
3.5.3 查看暂存区和版本库之间文件的差异
$ git diff --cached # 查看暂存区和最新版本之间的所有文件差异
$ git diff --cached -- file # 查看暂存区和最新版本之间的指定文件差异
$ git diff --cached commitID # 查看暂存区和指定版本之间的所有文件差异
$ git diff --cached commitID -- file # 查看暂存区和指定版本之间的指定文件差异
3.5.4 查看不同版本库之间文件的差异
$ git diff commitID1 commitID2 # 查看两个版本之间的所有文件的差异
$ git diff commitID1 commitID2 -- file1 file2 # 查看两个版本之间的指定文件的差异
$ git diff commitID1 commitID2 --stat # 查看两个版本之间的改动的文件列表
$ git diff commitID1 commitID2 src/ # 查看两个版本之间的 src 目录下所有文件的差异
3.5.5 git diff 的插件
我们除了可以使用 git diff 来分析文件差异外,也还可以使用图形化的工具或外部 diff 工具(如 Beyond Compare)来比较差异,具体配置方法如下:
-
使用
$ git difftool --tool-help
指令查看自己的 git 版本支持哪些工具:$ git difftool --tool-help ... The following tools are valid, but not currently available: araxis Use Araxis Merge (requires a graphical session) bc Use Beyond Compare (requires a graphical session) bc3 Use Beyond Compare (requires a graphical session) bc4 Use Beyond Compare (requires a graphical session) ... Some of the tools listed above only work in a windowed environment. If run in a terminal-only session, they will fail.
- bc3、bc4 代表的就是 Beyond Compare
-
修改配置文件:
$ git config --global diff.tool bc4 $ git config --global difftool.bc4.cmd '"D:\Program Files\BCompare\BComp.exe" "$LOCAL" "$REMOTE"'
- 注意是 BComp.exe 而非 BeyondCompare.exe
修改好配置文件后,只需要运行 $ git difftool -y
,就能自动打开 Beyond Compare 来显示差异的文件。
但假如你改动的文件很多,Beyond Compare 不会一起给你打开,而是一个一个打开(关掉一个,他再给你弹出来一个窗口),非常麻烦,这种情况下你就可以使用 $ git difftool --dir-diff
指令。
3.6 代码回退
如果在修改时发现修改有误,而要放弃本地修改时,你可以这么做...
已修改 & 未提交暂存
$ git checkout -- filepathname # 还原未提交到暂存区的某一具体已追踪文件的修改
$ git checkout -- . # 还原未提交到暂存区的所有已追踪文件的修改
$ git resete --hard HEAD # 撤销未提交到版本库的所有已追踪文件的修改
- 如
$ git checkout -- test.txt
,但不要忘记中间的「--」,不写就成了检出分支了。
已修改 & 已提交暂存
$ git restore --staged filepathname # 取消暂存
$ git checkout -- filepathname # 代码还原
已修改 & 提交本地仓库
$ git reset --hard HEAD^ # 回退到前一个版本
参考资料
- Git - Book (git-scm.com)
- Git教程 - 廖雪峰的官方网站 (liaoxuefeng.com)
- Learn Git Branching
- Git 的三个区域详解_git的三个区_白豆五的博客-CSDN博客