背景
版本控制这个东西的作用和意义,我就不啰嗦了。从 SVN 到 Git,我自己是没有认认真真地学习过的,平时就是照葫芦画瓢,反正能用。自己一个人搞的时候,基本上没啥问题;团队协作的时候就很容易遇到问题,最怕的场景就是 冲突,因为不懂原理,很多时候只能骚操作:
- 强制回退
- 拷贝代码
我才发现,不止我不懂,原来好多人其实都不懂。大家都把这玩意儿当一工具,能用就行。
关于 Git,你可能看到过《一张图就够了》,也可能看到过《权威指南》,好像这东西有时候很简单,有时候又很复杂。我更倾向于以基础理论为指导,贴近应用场景的最佳实践。
这篇文章就是干这个的。主要包括这些内容:
- 版本库的基本概念
- 版本管理:提交版本、比较版本和回退版本
- 分支管理:创建分支、切换分支、合并分支和删除分支,重点介绍合并冲突的解决方法
- 标签管理:创建标签和删除标签
- 远程推送和拉取
版本库
版本库是一个概括性地称呼,实际上由四部分组成:
代码文件所在的目录就是工作区,工作区目录内可以包含若干个文本文件,也可以包含若干个子目录。
版本管理仅对文本文件生效。
暂存区
工作区中代码文件编辑完成之后,需要添加到暂存区。
暂存区这个概念可能会让你很迷惑。据说 Git 设计之初,暂存区是作为一大特色存在的;从现在的角度看,意义已经没有那么大,你完全可以把它当作工作区和版本库之间的一个 中转站:
- 工作区中的文件,编辑好之后,可以添加到中转站,作为一个 临时版本 保存起来;
- 文件编辑过程中,可以多次添加到中转站,每一次都会覆盖中转站中该文件之前的版本;
- 文件编辑过程中,你如果后悔了,可以从中转站回退之前的版本;
- 中转站的文件随时都可以提交给版本库,作为一个永久版本保存起来。
不是工作区中所有编辑过的文件都需要添加暂存区,只把那些马上或不久之后需要提交到版本库的文件添加到暂存区即可,其它的文件可以以后再说。
本地版本库
位于暂存区中的代码文件,可以一次性地提交至本地版本库。提交完成之后,会清空暂存区。
远程版本库
多人协作时,就需要使用远程版本库,相较于本地版本库,没有什么实质性的不同,可以简单理解为本地版本库的一个共享拷贝,保存在一个特定的服务器上面。多个本地版本库和远程版本库之间可以通过 推送 和 拉取 的方式保持同步。
如果没有特殊说明,以下版本库均指本地版本库。
创建版本库
创建代码目录:
mkdir learngit
创建版本库:
cd learngit
git init
提示:
Initialized empty Git repository in /private/tmp/learngit/.git/
版本库创建成功。
Git 可能因版本不同,输出内容略有不同。
版本控制
版本提交
创建文件 README.md:
touch README.md
编辑文件 README.md:
vim README.md
输入三行文件:
a
b
c
保存退出。
我们可以使用命令 git status 随时查看版本库状态:
// 分支 master(分支的概念详见后文)
On branch master
No commits yet
// 未跟踪的文件列表,就是工作区中还没有添加到暂存区的文件列表
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
nothing added to commit but untracked files present (use "git add" to track)
git status 是一个特别有用的命令,它可以根据版本库的状态告诉我们下一步可以执行的操作,以及相应的命令。比如:把工作区中的文件添加到暂存区,可以使用命令 git add。
把工作区中的文件 README.md 添加到暂存区:
git add README.md
查看版本库状态:
On branch master
No commits yet
// 暂存区中的文件列表
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
可以使用命令 git rm 删除暂存区中的文件,该操作不会影响工作区中的文件。
把暂存区中的文件 README.md 提交到版本库:
git commit -m "first commit"
[master (root-commit) 907e2fe] first commit
1 file changed, 3 insertions(+)
create mode 100644 README.md
命令 git commit 会一次性把暂存区中的文件全部提交到版本库,如果某些文件不需要提交,可以使用命令 git rm 删除。
命令参数 m 的内容是提交注释,记录这一次提交更新了什么内容。
查看版本库状态:
On branch master
// 工作区是干净的
nothing to commit, working tree clean
这就是一个代码文件版本提交的所有过程,实际上只有两步:
git add <file> ...
git commit -m "..."
版本历史
每一次版本提交都会有唯一的版本号,而且会记录一条提交日志,我们可以使用命令 git log 查看版本的历史提交记录:
git log
commit 907e2feb517960193606d27fbb7641bcac81b2a6 (HEAD -> master)
Author: mmtb <>
Date: Wed Sep 28 18:01:05 2022 +0800
first commit
目前只有一次提交,907e2feb517960193606d27fbb7641bcac81b2a6 是这一次提交的版本号。
版本比较
我们可以在工作区、暂存区和版本库之间相互比较文件内容的差异。
编辑工作区中文件 README.md,修改文件内容如下:
aa
b
cc
d
把工作区中的文件 README.md 添加到暂存区:
git add README.md
再次编辑工作区中的文件 README.md,修改文件内容如下:
aaa
b
ccc
此时,对于同一个文件 README.md 而言,它在工作区、暂存区和版本库中分别有三个内容版本:
// 工作区
aaa
b
ccc
// 暂存区
aa
b
cc
d
// 版本库
a
b
c
比较内容差异可以使用命令 git diff。
工作区和暂存区比较
git diff
diff --git a/README.md b/README.md
index 10ef04f..e39f617 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-aa
+aaa
b
-cc
-d
+ccc
实际场景中你可以会看到多个类似内容的区块:
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-aa
+aaa
b
-cc
-d
+ccc
每一个区块表示两个文件之间一处内容不同。
--- a/README.md
+++ b/README.md
- 指代暂存区中的文件 README.md,+ 指代工作区中的文件 README.md。
@@ -1,4 +1,3 @@
@@ ... @@ 之间的内容表示两个文件之间哪些行的内容在做比较。-1,4 表示暂存区的文件,从第 1 行开始,连续 4 行;+1,3 表示工作区的文件,从第 1 行开始,连续 3 行。
-aa
+aaa
b
-cc
-d
+ccc
工作区的文件 README.md 相较于暂存区的文件 README.md 发生的内容变化:
// 删除
-aa
// 添加
+aaa
// 不变
b
// 删除
-cc
// 删除
-d
// 添加
+ccc
注意两个地方的 +/- 有不同的含义。
暂存区和版本库比较
git diff --cached
diff --git a/README.md b/README.md
index de98044..10ef04f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
-a
+aa
b
-c
+cc
+d
默认比较的是版本库当前分支的最新版本,相当于
git diff --cached HEAD
HEAD 表示版本库当前分支的最新版本,也可以使用具体的版本号。
工作区和版本库比较
git diff HEAD
diff --git a/README.md b/README.md
index de98044..e39f617 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
-a
+aaa
b
-c
+ccc
HEAD 表示版本库当前分支的最新版本,也可以使用具体的版本号。
版本库不同版本比较
把暂存区中的文件提供给版本库:
git commit -m "second commit"
[master c64957a] second commit
1 file changed, 3 insertions(+), 2 deletions(-)
查看版本提交历史记录:
git log
commit c64957a53041867d0d5f56b2fcfc4b814c97aa14 (HEAD -> master)
Author: mmtb <>
Date: Wed Sep 28 19:31:04 2022 +0800
second commit
commit 907e2feb517960193606d27fbb7641bcac81b2a6
Author: mmtb <>
Date: Wed Sep 28 18:01:05 2022 +0800
first commit
可以看到两个版本的提交记录,比较这两个版本:
git diff HEAD^ HEAD
HEAD 和 HEAD^ 分别表示版本库当前分支的最新版本和最新版本的上一个版本,也可以使用具体的版本号。
git diff 可以指定一个或多个文件名称,表示仅比较指定的这些文件。
版本回退
工作区的文件内容发生变化之后,如果想回退到之前的某个版本,有两种方式:
- 从暂存区回退版本
- 从版本库回退版本
暂存区回退版本
使用暂存区中的文件 README.md 回退工作区中的文件 README.md:
git checkout -- README.md
有两种情况:
- 暂存区中存在文件 README.md,使用暂存区中的文件 README.md 回退工作区中的文件 README.md;
- 暂存区中不存在文件 README.md,使用版本库中当前分支的最新版本中的文件 README.md 回退工作区的中文件 README.md。
版本库回退版本
使用版本库回退,情况要稍微复杂一些,可以有两种方式。
第一种方式:先从版本库的指定版本回退至暂存区,再从暂存区回退至工作区
从版本库的指定版本回退至暂存区:
git reset HEAD README.md
有两种情况:
- 如果版本是最新版本,会删除掉暂存区中的文件 README.md;
- 如果版本不是最新版本,会使用指定版本的文件 README.md 覆盖暂存区中的文件 README.md;
再从暂存区的文件 README.md 回退至工作区:
git checkout -- README.md
第二种方式:直接从版本库的指定版本回退
这种方式会同时影响暂存区和工作区,不同的回退模式对这两个区域的会有不同的回退结果,这里仅介绍 hard 模式:
git reset --hard HEAD
HEAD 表示版本库当前分支的最新版本,也可以使用具体的版本号。
hard 模式会同时重置(清空)暂存区和工作区,回退之后,暂存区中不包含任何文件,工作区中的所有文件和文件内容和指定的回退版本保持一致。
分支
假设你有一个文件 file,之前都是你自己一个人编辑,现在工作量比较大,安排张三和李四协助你,根据分工,每人负责文件的一部分内容。
张三和李四每个人拷贝一份文件 file,分别重命名为文件 file1 和文件 file2,三个人各自在自己的文件里编辑自己负责的那部分内容。
最开始的时候文件只有一份,拷贝之后,文件变成了三份,而且三份文件的内容这时是完全相同的。之后三个人会在各自的文件里编辑内容,相当于原先的一个文件会向三个不同的方向发生变化,这里的每一个方向就称之为一个 分支,拷贝文件就是 创建分支。
文件编辑过程中,可能需要相互帮忙修改一下对方的文件,你本来编辑的是文件 file,这时可能需要去编辑文件 file1,切换文件就是 切换分支。
文件编辑完成,会把三个文件的内容合并到一个文件里去,文件合并就是 合并分支。
如果三个文件的内容有重合,且不完成相同,合并的时候就需要特殊处理:选择某个人的内容还是整合三个人的内容,这个特殊处理就是 合并冲突。
文件合并完成之后,文件 file1 和 文件 file2 就可以删除了,文件删除就是 删除分支。
创建版本库的时候,Git 会为我们创建一个默认分支 master。版本库中的每一个分支的版本控制都是相互独立的。
默认分支名称可能是 master 或 main,也可以自定义。
创建分支
创建分支:
git checkout -b dev
命令执行完成之后,分支 dev 会被创建,且版本库的当前分支会由 master 自动切换至 dev。
查看分支:
git branch
* dev
master
分支名称前面的 * 标识着版本库的当前分支。
切换分支
切换分支:
git checkout master
Switched to branch 'master'
git checkout dev
Switched to branch 'dev'
你可能也注意到了,工作区的版本回退、创建分支和切换分支都用到了命令 git checkout,只是参数不同,很容易混淆。对于切换分支,Git 提供了专用命令 git switch:
git switch master
Switched to branch 'master'
git switch dev
Switched to branch 'dev'
合并分支
文件 README.md 在分支 master 和 dev 的内容是一样的:
aa
b
cc
d
切换到分支 dev,编辑文件 README.md 内容:
aa
bb
cc
dd
保存文件,添加到暂存区,提交到版本库:
git add README.md
git commit -m "dev commit"
[dev eb2f412] dev commit
1 file changed, 2 insertions(+), 2 deletions(-)
这些操作都是在分支 dev 上面进行的。
现在需要把分支 dev 上面的文件内容变更合并到分支 master 上面,切换分支:
git switch master
合并分支:
git merge dev
Updating c64957a..eb2f412
Fast-forward
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
查看文件 README.md:
aa
bb
cc
dd
分支合并成功。
合并冲突
切换分支为 master,修改文件 README.md:
a
bb
cc
dd
保存文件,添加到暂存区,提交到版本库。
切换分支为 dev,修改文件 README.md:
aaa
bb
cc
dd
保存文件,添加到暂存区,提交到版本库。
分支 master 和 dev 的文件 README.md 的第一行内容不同。
切换分支为 master,合并分支 dev:
git merge dev
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
其中,
CONFLICT (content): Merge conflict in README.md
这句话表示分支合并过程中,README.md 出现合并冲突。
使用命令 git status 查看版本库状态:
git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: README.md
no changes added to commit (use "git add" and/or "git commit -a")
可以看到有两个重要的信息:
- 存在未合并的文件,可以使用命令 git merge --abort 可以终止合并过程,相当于退回到合并之前的状态;
- 未合并的文件路径,解决冲突之后,可以使用命令 git add \
... 标记冲突已解决。
查看文件 README.md:
<<<<<<< HEAD
a
=======
aaa
>>>>>>> dev
bb
cc
dd
文件里每一个类似这样的区块:
<<<<<<< HEAD
a
=======
aaa
>>>>>>> dev
表示该文件在合并过程中存在的一个冲突。
使用
=======
分隔成两部分,上半部分是分支 master 的内容,下半部分是分支 dev 的内容。
我们需要比对这两部分的内容,决定应该如何修改这个冲突的区块:
aa
bb
cc
dd
修改完成之后,保存文件,标记该文件冲突已解决:
git add README.md
再查看版本库状态:
git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: README.md
提示冲突已解决,但仍处理合并中,需要提交到版本库,才能完成合并:
git commit -m "merge"
[master 8b4bda1] merge
总结一下,合并冲突有两种解决方案:
- 终止合并
- 修改冲突文件,标记文件冲突已解决,提交到版本库。
删除分支
删除分支:
git branch -d dev
命令执行完成之后,分支 dev 已被删除。
标签
在代码的开发过程中,如果某个版本对我们具有 里程碑 式的意义,我们想把这个版本以特殊的方式标记下来,使用版本号是一种可选的方式。可是版本号是一串无意义的数字,非常不利于阅读和记忆,如果能够给版本号起一个人性化的名字就好了,这个名字就是 标签。
创建标签
创建标签:
git tag 0.1
命令执行完成之后,标签 0.1 已创建。
查看标签:
git tag
0.1
删除标签
删除标签:
git tag -d 0.1
Deleted tag '0.1' (was 8b4bda1)
命令执行完成之后,标签 0.1 已删除。
远程版本库
如果多人之间需要共享版本库,就需要创建一个远程版本库,把自己的本地版本库和远程版本库建立链接,就可以使用 推送 和 拉取 的方式保持两者之间的同步。
版本库
我们在 gitee 上面创建一个远程版本库 learngit,链接本地版本库和远程版本库:
git remote add origin git@gitee.com:cutecatdad/learngit.git
origin 是远程仓库惯用的名称。
分支
推送本地分支
切换到本地分支 master:
git checkout master
推送本地分支 master 到远程仓库,第一次推送:
git push -u origin master
命令执行完成之后,远程版本库会创建名称为 master 的远程分支,后续再推送时可以使用命令的简化形式:
git push
推送过程会包含分支合并,如果提示出现冲突,需要先拉取远程分支(见下文),解决冲突(参考合并冲突)之后,再次推送。
拉取远程分支
切换到本地分支 master:
git checkout master
拉取远程分支 master 至本地分支 master:
git pull
分支拉取过程包含分支合并,如果提示出现冲突,解决冲突(参考合并冲突)之后,及时推送。
删除远程分支
删除远程分支:
git push origin :dev
命令执行完成之后,远程分支 dev 已删除。
查看远程分支
仅查看远程分支:
git branch -r
origin/dev
origin/master
查看本地分支和远程分支:
git branch -a
dev
* master
remotes/origin/dev
remotes/origin/master
标签
查看远程分支
仅查看远程分支:
git ls-remote --tags
From git@gitee.com:cutecatdad/learngit.git
f6f9c715233c118769bfaaa30fb297a8284c4f61 refs/tags/0.1
推送本地标签和删除远程标签的命令和分支类似,不再赘述。
结束
Git 是一个非常灵活好用的版本控制工具,但也只是一个工具,如果需要掌握全部的命令和参数难度还是挺大的,也没什么实际意义。实际场景中最好还是根据自己的情况搞一套简洁明了的 最佳实践,尽可能用最少最简洁的命令完成版本控制的多人协作,这就是另一个话题了。