1.初识 Git
在软件开发过程中,为了防止文档丢失或更改失误后能恢复到原来的版本,常需复制多个副本。随着版本数量增多,管理不同版本的修改变得困难。为了解决这个问题,便有了版本控制器。版本控制器是记录工程每一次改动和版本迭代的管理系统,方便多人协同作业。
目前最主流的版本控制器是 Git。Git 可以控制电脑上所有格式的文件,对于开发人员来说,最重要的是管理软件开发项目中的源代码文件。
注意事项:所有的版本控制系统(包括 Git)只能跟踪文本文件的改动,如 TXT、网页、程序代码等。版本控制系统可以告诉你每次的改动细节。而图片、视频等二进制文件,虽然能被管理,但无法跟踪具体变化,只能记录文件大小变更。
2.Git 安装
Git 是开放源代码的代码托管工具,最早在 Linux 下开发,现已支持 Linux、Unix、Mac 和 Windows 平台。
2.1 Linux—centos 系统安装 Git
以 CentOS 7.6 为例。首先输入 git 查看是否已安装。若显示 bash: git: command not found,则未安装。
sudo yum -y install git
git --version
2.2 Linux—ubuntu 系统安装 Git
以 Ubuntu 22.04 为例。输入 git 查看。若提示 Command 'git' not found,可进行安装。
sudo apt-get install git -y
git --version
2.3 Windows 系统安装 Git
Windows 环境下可通过官方安装包进行安装,具体步骤参考官方文档。
3.Git 基本操作
3.1 创建 Git 本地仓库
仓库是进行版本控制的一个文件目录。创建 Git 本地仓库对应的命令为 git init,注意命令要在文件目录下执行。
执行后会发现当前目录下多了一个 .git 的隐藏目录。.git 目录是 Git 来跟踪管理仓库的,包含 Git 仓库的诸多细节,不要手动修改里面的文件。
3.2 配置 Git 本地仓库
安装 Git 后首先要设置用户名和邮箱地址。配置命令为:
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
--global 是可选项。如果使用该选项,表示这台机器上所有的 Git 仓库都会使用这个配置。如果不希望全局配置,可去掉该选项,但必须在仓库里执行。
查看配置命令:git config -l
删除对应配置命令:
git config [--global] --unset user.name
git config [--global] --unset user.email
3.3 认识工作区、暂存区、版本库
- 工作区:是在电脑上你要写代码或文件的目录。
- 暂存区:英文叫 stage 或 index。一般存放在
.git目录下的index文件中,有时也叫作索引 (index)。 - 版本库:又名仓库,英文名 repository。工作区有一个隐藏目录
.git,它不算工作区,而是 Git 的版本库。这个版本库里面的所有文件都可以被 Git 管理起来。
工作流程说明:
- 在创建 Git 版本库时,Git 会自动创建一个唯一的 master 分支,以及指向 master 的一个指针叫 HEAD。
- 当对工作区修改(或新增)的文件执行
git add命令时,暂存区目录树的文件索引会被更新。 - 当执行提交操作
git commit时,master 分支会做相应的更新,暂存区的目录树才会被真正写到版本库中。
通过新建或粘贴进目录的文件,并不能称之为向仓库中新增文件,只是在工作区新增了文件。必须要通过使用 git add 和 git commit 命令才能将文件添加到仓库中进行管理。
3.4 添加文件—场景一
在包含 .git 的目录下新建一个 ReadMe 文件,可以使用 git add 命令将文件添加到暂存区:
- 添加一个或多个文件到暂存区:
git add [file1] [file2] ... - 添加指定目录到暂存区,包括子目录:
git add [dir] - 添加当前目录下的所有文件改动到暂存区:
git add .
再使用 git commit 命令将暂存区内容添加到本地仓库中:
- 提交暂存区全部内容到本地仓库中:
git commit -m "message" - 提交暂存区的指定文件到仓库区:
git commit [file1] [file2] ... -m "message"
注意:git commit 后面的 -m 选项,要跟上描述本次提交的 message,由用户自己完成,这部分内容绝对不能省略,是用来记录你的提交细节。
执行成功后会告诉我们文件被改动的情况。我们可以多次 add 不同的文件,而只 commit 一次便可以提交所有文件。
截至目前为止,我们已经能够将代码直接提交至本地仓库了。可以使用 git log 命令来查看历史提交记录。
该命令显示从最近到最远的提交日志。如果嫌输出信息太多,可以加上 --pretty=oneline 参数。
看到的类似 11ae5079a637... 的是每次提交的 commit id(版本号)。Git 的 commit id 不是递增的数字,而是一个 SHA1 计算出来的非常大的数字,用十六进制表示。
3.5 查看.git 文件
.git 目录结构说明:
- index:就是我们的暂存区,
add后的内容都是添加到这里的。 - HEAD:默认指向 master 分支的指针。
- refs/heads/master:文件里保存当前 master 分支的最新 commit id。
- objects:包含了创建的各种版本库对象及内容,可以简单理解为放了 Git 维护的所有修改。
查找 object 时要将 commit id 分成 2 部分,其前 2 位是文件夹名称,后 38 位是文件名称。找到这个文件之后,一般不能直接看到里面是什么,该类文件是经过 sha (安全哈希算法) 加密过的文件。好在我们可以使用 git cat-file 命令来查看版本库对象的内容。
总结:在本地的 git 仓库中,有几个文件或目录很特殊:
- index:暂存区,git add 后会更新该内容。
- HEAD:默认指向 master 分支的一个指针。
- refs/heads/master:文件里保存当前 master 分支的最新 commit id。
- objects:包含了创建的各种版本库对象及内容。
3.6 添加文件—场景二
示例展示另一种添加文件的场景。提交后发现打印了 1 file changed, 0 insertions(+), 0 deletions(-),意思是只有一个文件改变了。这是因为我们并没有使用 git add file5,file5 就不在暂存区中维护,所以我们 commit 的时候其实只是把已经在暂存区的 file4 提交了,而遗漏了工作区的 file5。如何提交 file5 呢?很简单,再次 add,commit 即可。
3.7 修改文件
Git 跟踪并管理的是修改,而非文件。什么是修改?比如你新增了一行,这就是一个修改,删除了一行,也是一个修改。
让我们将 ReadMe 文件进行一次修改:添加一行 hello world。
此时,仓库中的 ReadMe 和工作区的 ReadMe 是不同的。如何查看当前仓库的状态呢?git status 命令用于查看在你上次提交之后是否有对文件进行再次修改。
如果想知道具体哪些地方被修改了,可以使用 git diff [file] 命令用来显示暂存区和工作区文件的差异。也可以使用 git diff HEAD -- [file] 命令来查看版本库和工作区文件的区别。
知道了对 ReadMe 做了什么修改后,再把它提交到本地仓库就放心多了。git add 之后,就没有看到 no changes added to commit 的消息了。接下来继续 git commit 即可。
3.8 版本回退
如果有某一天发现之前做的工作出现了很大的问题,需要在某个特定的历史版本重新开始,这个时候,就需要版本回退的功能了。
执行 git reset 命令用于回退版本,可以指定退回某一次提交的版本。要解释一下'回退'本质是:要将版本库中的内容进行回退,工作区或暂存区是否回退由命令参数决定:
git reset 命令语法格式为:git reset [--soft | --mixed | --hard] [HEAD]
--mixed为默认选项,使用时可以不用带该参数。该参数将暂存区的内容退回为指定提交版本内容,工作区文件保持不变。--soft参数对于工作区和暂存区的内容都不变,只是将版本库回退到某个指定版本。--hard参数将暂存区与工作区都退回到指定版本。切记工作区有未提交的代码时不要用这个命令,因为工作区会回滚,你没有提交的代码就再也找不回了,所以使用该参数前一定要慎重。
HEAD 说明:
- 可直接写成 commit id,表示指定退回的版本。
- HEAD 表示当前版本
- HEAD^ 上一个版本
- HEAD^^ 上上一个版本
- 以此类推…
也可以使用 〜数字表示:
- HEAD~0 表示当前版本
- HEAD~1 上一个版本
- HEAD~2 上上一个版本
- 以此类推…
当我们进行清屏操作时,或者将服务器关闭了,那么 commit 展示的信息就不见了,还想进行版本回退该如何解决呢?Git 还提供了一个 git reflog 命令能补救一下,该命令用来记录本地的每一次命令。这样,你就可以很方便地找到你的所有操作记录了!Git 版本回退的时候,也可以使用部分 commit id 来代表目标版本。
在实际开发中,由于长时间的开发,导致 commit id 早就找不到了,可突然某一天,又想回退到我想要的版本那该如何操作呢?Git 的版本回退速度非常快,因为 Git 在内部有个指向当前分支 (此处是 master) 的 HEAD 指针,refs/heads/master 文件里保存当前 master 分支的最新 commit id。当我们在回退版本的时候,Git 仅仅是给 refs/heads/master 中存储一个特定的版本。
3.9 撤销修改—情况一
如果在工作区写了很长时间代码,越来越写不下去,想恢复到上一个版本。
情况一:对于工作区的代码,还没有 add。
我们在 ReadMe 文件中新增一行代码,然后再删除代码!虽然可以直接删除,但如果写了很久才发现不行,该怎么删掉呢?Git 其实还提供了更好的方式,我们可以使用 git checkout -- [file] 命令让工作区的文件回到最近一次 add 或 commit 时的状态。要注意 git checkout -- [file] 命令中的 -- 很重要,切记不要省略,一旦省略,该命令就变为其他意思了。
3.10 撤销修改—情况二
情况二:已经 add,但没有 commit。
add 后还是保存到了暂存区呢?怎么撤销呢?回忆一下学过的 git reset 回退命令,该命令如果使用 --mixed 参数,可以将暂存区的内容退回为指定的版本内容,但工作区文件保持不变。那我们就可以回退下暂存区的内容了。用 git status 查看一下,发现现在暂存区是干净的,工作区有修改。如何丢弃工作区的修改吗?
3.11 撤销修改—情况三
情况三:已经 add,并且也 commit 了
不要担心,我们可以 git reset --hard HEAD^ 回退到上一个版本!不过,这是有条件的,就是你还没有把自己的本地版本库推送到远程。还记得 Git 是分布式版本控制系统吗?一旦你推送到远程版本库,你就真的惨了…
3.12 删除文件
在 Git 中,删除也是一个修改操作。如果要删除 file5 文件,怎么搞呢?如果你直接删除了文件,但这样直接删除是没有用的,反而徒增烦恼,git status 命令会立刻告诉你哪些文件被删除了。
此时,工作区和版本库就不一致了,要删文件,目前除了要删工作区的文件,还要清除版本库的文件。
一般走到这里,有两种可能:
- 确实要从版本库中删除该文件
- 不小心删错了
对于第二种情况,很明显误删,需要使用 git 来进行恢复。对于第一种情况,很明显是没有删完,我们只删除了工作区的文件。这时就需要使用 git rm 将文件从暂存区和工作区中删除,并且 commit。
4.分支管理
4.1 理解分支
Git 的杀手级功能之一:分支。分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习 C++ 的时候,另一个你正在另一个平行宇宙里努力学习 JAVA。如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了 C++ 又学会了 JAVA!
在版本回退里,你已经知道,每次提交,Git 都把它们串成一条时间线,这条时间线就可以理解为一个分支。截止到当前,只有一条时间线,在 Git 里,这个分支叫主分支,即 master 分支。再来理解一下 HEAD,HEAD 严格来说不是指向提交,而是指向 master,master 才是指向提交的,所以,HEAD 指向的就是当前分支。
4.2 创建、切换、合并分支
-
创建分支 Git 支持我们查看或创建其他分支,在这里我们来创建第一个自己的分支 dev,对应的命令为:
git branch dev当我们创建新的分支后,Git 新建了一个指针叫 dev,* 表示当前 HEAD 指向的分支是 master 分支。 发现目前 dev 和 master 指向同一个修改。并且也可以验证下 HEAD 目前是指向 master 的。
-
切换分支 那如何切换到 dev 分支下进行开发呢?使用
git checkout命令即可完成切换。git checkout dev我们发现 HEAD 已经指向了 dev,就表示我们已经成功的切换到了 dev 上!接下来,在 dev 分支下修改 ReadMe 文件,新增一行内容,并进行一次提交操作。 现在,dev 分支的工作完成,我们就可以切换回 master 分支:
git checkout master切换回 master 分支后,发现 ReadMe 文件中新增的内容不见了!!!赶紧再切回 dev 看看:
git checkout dev在 dev 分支上,内容还在。为什么会出现这个现象呢?我们来看看 dev 分支和 master 分支指向,发现两者指向的提交是不一样的。 看到这里就能明白了,因为我们是在 dev 分支上提交的,而 master 分支此刻的提交点并没有变。 当切换到 master 分支之时,HEAD 就指向了 master,当然看不到提交了!
-
合并分支 为了在 master 主分支上能看到新的提交,就需要将 dev 分支合并到 master 分支。
git merge devgit merge命令用于合并指定分支到当前分支。合并后,master 就能看到 dev 分支提交的内容了。 Fast-forward 代表'快进模式',也就是直接把 master 指向 dev 的当前提交,所以合并速度非常快。当然,也不是每次合并都能 Fast-forward。
4.3 删除分支
合并完成后,dev 分支对于我们来说就没用了,那么 dev 分支就可以被删除掉。注意如果当前正处于某分支下,就不能删除当前分支。 但是可以在其他分支下删除当前分支。 因为创建、合并和删除分支非常快,所以 Git 鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在 master 分支上工作效果是一样的,但过程更安全。
4.4 合并冲突
可是,在实际分支合并的时候,并不是想合并就能合并成功的,有时候可能会遇到代码冲突的问题。为了演示这问题,创建一个新的分支 dev1,并切换至目标分支,我们可以使用 git checkout -b dev1 一步完成创建并切换的动作。
在 dev1 分支下修改 ReadMe 文件,更改文件内容如下,并进行一次提交。 切换至 master 分支,观察 ReadMe 文件内容:我们发现,切回来之后,文件内容由变成了老的版本,这种现象很正常。 此时在 master 分支上,我们对 ReadMe 文件再进行一次修改,并进行提交。 现在,master 分支和 dev1 分支各自都分别有新的提交,变成了这样。 这种情况下,Git 只能试图把各自的修改合并起来,但这种合并就可能会有冲突。
发现 ReadMe 文件有冲突后,可以直接查看文件内容。要说的是 Git 会用 <<<<<<<, =======, >>>>>>> 来标记出不同分支的冲突内容。
此时我们必须要手动调整冲突代码,并需要再次提交修正后的结果!!(再次提交很重要,切勿忘记)
到这里冲突就解决完成。用带参数的 git log 也可以看到分支的合并情况,具体大家可以自行搜索 git log 的用法。
最后,不要忘记 dev1 分支使用完毕后就可以删除了。
4.5 分支模式
通常合并分支时,如果可能,Git 会采用 Fast forward 模式。在这种 Fast forward 模式下,删除分支后,查看分支历史时,会丢掉分支信息,看不出来最新提交到底是 merge 进来的还是正常提交的。
但在合并冲突部分,我们也看到通过解决冲突问题,会再一次进行新的提交,得到的最终状态就不是 Fast forward 模式了。这样的好处是,从分支历史上就可以看出分支信息。
Git 支持我们强制禁用 Fast forward 模式,那么就会在 merge 时生成一个新的 commit,这样,从分支历史上就可以看出分支信息。下面实战一下 --no-ff 方式的 git merge。
首先,创建新的分支 dev2,并切换至新的分支。修改 ReadMe 文件,并提交一个新的 commit。切回 master 分支,开始合并。
请注意 --no-ff 参数,表示禁用 Fast forward 模式。禁用 Fast forward 模式后合并会创建一个新的 commit。所以加上 -m 参数。把描述写进去。合并后,查看分支历史。
可以看到,不使用 Fast forward 模式,merge 后就像这样。所以在合并分支时,加上 --no-ff 参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而 fast forward 合并就看不出来曾经做过合并。
4.6 分支策略
在实际开发中,我们应该按照几个基本原则进行分支管理: 首先,master 分支应该是比较稳定的,也就是仅用来发布新版本,平时不能在上面干活; 那在哪干活呢?干活都在 dev 分支上,也就是说,dev 分支是不稳定的,到某个时候,比如 1.0 版本发布时,再把 dev 分支合并到 master 上,在 master 分支发布 1.0 版本;你和你的小伙伴们每个人都在 dev 分支上干活,每个人都有自己分支,时不时地往 dev 分支上合并就可以了。
4.7 bug 分支
假如我们现在正在 dev2 分支上进行开发,开发到一半,突然发现 master 分支上面有 bug,需要解决。在 Git 中,每个 bug 都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
可现在 dev2 的代码在工作区中开发了一半,还无法提交,怎么办?例如:
Git 提供了 git stash 命令,可以将当前工作区信息进行储藏,被储藏的内容可以在将来某个时间恢复出来。
用 git status 查看工作区,就是干净的 (除非有没有被 Git 管理的文件),因此可以放心地创建分支来修复 bug。储藏 dev2 工作区之后,由于我们要基于 master 分支修复 bug,所以需要切回 master 分支,再新建临时分支来修复 bug。
修复完成后,切换到 master 分支,并完成合并。
至此,bug 的修复工作已经做完了,我们还要继续回到 dev2 分支进行开发。切换回 dev2 分支。
工作区是干净的,刚才的工作现场存到哪去了?用 git stash list 命令看看。
工作现场还在,Git 把 stash 内容存在某个地方了,但是需要恢复一下,如何恢复现场呢?我们可以使用 git stash pop 命令,恢复的同时会把 stash 也删了。
恢复完代码之后我们便可以继续完成开发,开发完成后便可以进行提交。
但我们注意到了,修复 bug 的内容,并没有在 dev2 上显示。此时的状态图为:Master 分支目前最新的提交,是要领先于新建 dev2 时基于的 master 分支的提交的,所以我们在 dev2 中当然看不见修复 bug 的相关代码。
我们的最终目的是要让 master 合并 dev2 分支的,那么正常情况下我们切回 master 分支直接合并即可,但这样其实是有一定风险的。是因为在合并分支时可能会有冲突,而代码冲突需要我们手动解决 (在 master 上解决)。我们无法保证对于冲突问题可以正确地一次性解决掉,因为在实际的项目中,代码冲突不只一两行那么简单,有可能几十上百行,甚至更多,解决的过程中难免手误出错,导致错误的代码被合并到 master 上。
解决这个问题的一个好的建议就是:最好在自已的分支上合并下 master,再让 master 去合并 dev,这样做的目的是有冲突可以在本地分支解决并进行测试,而不影响 master。
4.8 删除临时分支
软件开发中,总有无穷无尽的新的功能要不断添加进来。添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个分支,我们可以将其称之为 feature 分支,在上面开发,完成后,合并,最后,删除该 feature 分支。
可是,如果我们今天正在某个 feature 分支上开发了一半,被产品经理突然叫停,说是要停止新功能的开发。虽然白干了,但是这个 feature 分支还是必须就地销毁,留着无用了。这时使用传统的 git branch -d 命令删除分支的方法是不行的。
小结: 分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了 50% 的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。现在有了分支,就不用怕了。你创建了一个属于自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。并且 Git 无论创建、切换和删除分支,Git 在 1 秒钟之内就能完成!无论你的版本库是 1 个文件还是 1 万个文件。


