作者:Bezier\链接:https://juejin.cn/post/689524...前言Git作为世界上最强大的代码管理工具,相信大家都不陌生,但据我所知有很多A组人停留在clone,commit,pull,push...的阶段,对rebase没有概念的时候还敢用merge吗?遇到版本回滚就抓瞎?不要问我怎么知道的,就问:“我以前就是这样的~~”。针对这些问题,今天分享一下自己这几年对Git的认知和理解,尽可能从本质上讲解Git,一步步帮助大家理解Git的底层原理。相信看完本文后,你能以不一样的姿态,更加风骚地使用各种Git命令。目录的基本概念1.1Git的优点1.2文件状态1.3提交节点1.4HEAD1.5远程仓库分支2.1什么是分支?命令详解3.1Commit相关3.2Branch相关3.3Merge相关3.4Rollback相关3.5Remote相关一、基本概念1.1Git的优点Git是一个分布式代码管理工具。在讨论分布式代码之前,免不了要提到什么是中央代码中央仓库管理:所有代码都存储在中央服务器上,所以提交必须依赖网络,每次提交都会被带入中央仓库。如果是协同开发,可能会频繁触发代码合并,增加提交的成本和价格。最典型的就是svn分布式:可以不依赖网络在本地提交,每次提交都会自动在本地备份。每个开发者都可以克隆一份远程仓库到本地,并将提交历史汇集在一起??。代表就是Git,那么Git相对于svn有什么优势呢?比如:“巴拉巴拉写了很多代码,突然发现写的有问题,想回到一个小时前。”这种情况下,Git的优势就很明显了,因为commit的成本比较小,而且本地会保留所有的提交记录,随时回滚。这并不是说svn不能完成这种操作,而是Git的fallback会更优雅。与中心化工具相比,Git有很多优势,我就不一一列举了。有兴趣的可以自行学习。1.2文件状态在Git中,文件大致分为三种状态:修改(modified)、暂存(staged)、提交(committed)。修改:Git可以感知工作目录下哪些文件被修改,然后将修改的内容添加到暂存区:通过add命令将工作目录下修改后的文件提交到暂存区,并等待commit提交:将暂存区的文件提交到Git目录中永久存放1.3commit节点为了表述方便,本文中我将使用节点来指代commit提交。Git中每次提交都会生成一个节点,每个节点都会有一个哈希值作为唯一标识。多次提交会形成一个线性的节点链(不考虑合并情况),如上图1-1所示节点为SHA1计算的哈希值。节点C2包含C1的提交内容,节点C3包含C1和C2的提交内容。1.4HEADHEAD是Git中一个非常重要的概念。你可以称它为指针或Reference,它可以指向任意节点,指向的节点永远是当前工作目录。也就是说,当前工作目录(也就是你看到的代码)就是HEAD指向的节点。同样以图1-1为例,如果HEAD指向C2,则工作目录对应C2节点。如何移动HEAD点后面会说到,这里就不要纠结了。同时HEAD也可以指向一个分支,间接指向该分支指向的节点。1.5远程仓库Git虽然会将代码和历史记录保存在本地,但最终还是要提交到服务器上的远程仓库。远程仓库的代码可以通过clone命令下载到本地,提交历史、分支、HEAD等状态也会同步到本地,但是这些状态不会实时更新,需要手动从远程仓库拉取。什么时候拉,怎么拉,后面章节会讲到。通过远程仓库作为中介,可以与同事进行协同开发。开发新功能后,可以申请提交到远程仓库,也可以从远程仓库拉取同事的代码。要小心,因为你和你的同事会以远程仓库的代码为基准,所以一定要时刻保证远程仓库的代码质量,切记不要提交未经测试的代码到远程仓库2.分支2.1什么是a分支?分支也是Git中一个非常重要的概念。当一个分支指向一个节点时,当前节点的内容就是该分支的内容。它的概念和HEAD很接近,也可以看作是一个指针或者引用。不同的是branch可以有多个,而HEAD只有一个。通常根据功能或者版本建立不同的分支,那么分支有什么用呢?例如:你的App历经千辛万苦,终于发布了v1.0版本。由于急需,v1.0上线后马不停蹄的启动了v1.1。你的开发在上升期的时候,QA同学说,用户反馈了一些bug,需要修复,重新发布。修复v1.0必须基于v1.0的代码,但是你已经开发了v1.1的一部分,现在怎么办?通过引入分支的概念,可以优雅地解决以上问题。如图2-1所示,先看左边的原理图。假设C2节点为v1.0版本代号。上线后,在C2的基础上新建分支ft-1.0,然后看右边的示意图,v1.0上线后,可以在master分支开发v1.1内容,提交v1.1代码在收到QA同学反馈后生成节点C3,然后切换到ft-1.0分支进行bug修复,修复完成后提交代码生成节点C4,然后切换到master分支合并ft-1.0分支。至此,我们已经解决了上面的问题。此外,我们可以使用分支来做很多事情。比如有一个需求不确定是否要上线,但是你必须先做。这时候可以单独创建一个分支来开发这个功能。当需要上线时,直接合并到主分支即可。分支适用的场景有很多,就不一一列举了。注意,在某个节点创建分支时,不会复制该节点对应的代码,而是将新分支指向该节点,这样可以大大减少空间开销。一定要记住,不管是HEAD还是分支,都只是引用,量级很轻。通过命令add向暂存区添加一个文件:gitaddfilepath将所有文件添加到暂存区:gitadd。同时Git还提供了undoworkspace和暂存区命令来撤销工作区的修改:gitcheckout--文件名清空暂存区:gitresetHEAD文件名提交:将改变的文件添加到暂存区后,你可以提交它。提交后,会生成一个新的提交节点。具体命令如下:gitcommit-m"thenode"3.2Branch相关分支创建的描述信息创建分支后,该分支会指向与HEAD相同的节点。通俗一点就是新建的分支会指向HEAD指向的地方。命令如下:gitbranch分支名称switchbranch切换分支后,默认情况下,HEAD会指向当前分支,即HEAD间接指向当前分支所指向的节点。gitcheckoutbranchname也可以创建分支并立即切换。命令如下:gitcheckout-b分支名删除分支保证仓库分支的简洁性,当一个分支完成它的使命时,应该删除它。比如上面说的,开一个单独的分支来完成某个功能。当该功能合并到主分支时,应及时删除该分支。删除命令如下:gitbranch-d分支名3.3合并合并相关的命令合并是最难掌握的,也是最重要的。常用的合并命令大概有3种:merge、rebase、cherry-pickmergemerge是最常用的合并命令,可以将某个分支或者某个节点的代码合并到当前分支中。具体命令如下:gitmerge分支名/节点哈希值如果要合并的分支完全在当前分支之前,如图3-1所示,因为分支ft-1完全在分支ft之前-2,即ft-1完全包含ft-2,所以ft-2在执行“gitmergeft-1”后会触发快进(fastmerge)。此时两个分支指向同一个节点,是最理想的状态。但在实际开发中,我们经常会遇到如下情况:如图3-2(左)所示,这种情况不能直接合并。当ft-2执行“gitmergeft-1”时,Git会将节点C3和C4合并生成一个新的节点C5,最后将ft-2指向C5如图3-2(右)所示。注意:如果C3和C4同时修改了同一个文件中的同一行代码,此时合并会报错,因为Git不知道以哪个节点为标准,所以需要我们手动合并此时的代码。Rebase也是一个合并命令。命令行如下:gitrebase分支名/节点哈希值与merge不同的是,rebasemerge看起来不会生成新的节点(实际上会生成,只是做了一份拷贝),但是节点需要合并的会直接累加起来,如图3-3所示。左示意图中的ft-1.0在执行gitrebasemaster时,会在C3后面的C4节点复制一份,即C4',C4对应C4',只是hash值不同。与mergecommithistory相比,rebase更加线性和干净,让并行的开发过程看起来像串行的,更符合我们的直觉。既然rebase这么好用,难道merge可以丢掉吗?事实上,它不再是了。下面我将列举一些merge和rebase的优缺点:Merge的优缺点:优点:每个节点严格按照时间排列。当发生merge冲突时,只需要解决两个分支指向的节点之间的冲突即可随着时间的推移混乱。rebase的优缺点:优点:会让提交历史看起来更线性、干净缺点:虽然提交看起来是线性的,但并不是真正按照时间排序。例如图3-3中,无论C4比C3先提交还是晚于C3提交,最终都会落后于C3。而当merge出现冲突时,理论上可能会有几个节点rebase到目标分支来处理多个冲突。对于网上一些只使用rebase的观点,笔者并不认同。如果使用rebase来合并不同的分支,可能需要重复。解决冲突,使收益大于损失。但是如果是本地推送到远程,对应同一个分支,可以优先使用rebase。所以我的观点是根据不同的场景合理组合使用merge和rebase。如果你觉得没问题,那就用rebasecherry-pick。Cherry-pick的merge不同于merge和rebase。可以选择某些节点进行合并,如图3-4命令行:gitcherry-picknodehashvalue假设当前分支为master,执行gitcherry-pick后C3(hash值)、C4(hash值)命令,会直接抓取C3和C4节点放在后面,对应C3'和C4'3.4回滚相关分离HEAD默认情况下,HEAD指向一个分支,但是你也可以去掉一个分支的HEAD,直接指向一个节点。这个过程是为了分离HEAD。具体命令如下:gitcheckoutnodehashvalue//也可以直接离开分支指向当前节点。根据一个特殊的位置(branch/HEAD)提供HEAD,直接指向前一个或前N个节点的命令,即相对引用,如下://HEAD分隔并指向前一个节点gitcheckoutbranchname/HEAD^//HEAD分开指向前N个节点gitcheckoutbranchname~N分开HEAD指向节点有什么用?例如:如果开发过程中发现之前的提交有问题,此时可以将HEAD指向对应的节点,修改后再提交。这时候你肯定不想生成新的节点,只需要在提交时加上--amend即可,具体命令如下:gitcommit--amendrollback回滚场景在平时开发中比较常见.比如你写了很多代码提交了,后来发现写的有问题,所以你想把代码退回之前的提交。这种情况可以通过重置来解决。具体命令如下://返回N个提交gitresetHEAD~Nreset和相对引用很相似,不同的是reset会让分支和HEAD一起回去。3.5远程关联当我们接触一个新的项目时,首先要做的就是把它的代码记下来。在Git中,我们可以通过clone从远程仓库复制一段代码到本地。具体命令如下:仓库地址前的gitclone我在章节中也提到clone不仅复制了代码,还会去掉远程仓库的引用(branch/HEAD)并保存到本地,如图图3-5中:其中origin/master和origin/ft-1是远程仓库的分支,远程引用状态不会实时更新到本地。比如在远程仓库的origin/master分支上增加了一个commit。这个时候本地是没有察觉的,所以本地的origin/master分支还是指向了C4节点。我们可以使用fetch命令手动更新远程仓库的状态。Tips:只有存在于服务器上才能称为远程仓库。也可以克隆一个本地仓库作为远程仓库。当然,我们在实际开发中是不可能把本地仓库当成公共仓库的,这只是为了帮助你更清楚地理解分布式抓取。fetch命令是一个下载操作。它将新添加的远程节点和引用(branch/HEAD)的状态下载到本地。具体命令如下:gitfetch远程仓库地址/分支名pullpull命令可以从远程仓库中的引用中拉取代码。具体命令如下:gitpull远程分支名其实pull的本质就是fetch+merge。首先将远程仓库的状态全部更新到本地,然后合并。合并完成后,本地分支会指向最新的节点。另外,pull命令也可以通过rebase进行合并。具体命令如下:gitpull--rebase远程分支名push推送命令可以将本地的提交推送到远程,具体命令如下:gitpush远程分支名如果直接推送,可能会失败,因为可能会有冲突,所以往往先拉后推,如果有冲突就在本地解决。推送成功后,会更新本地远程分支引用,指向与本地分支相同的节点。综上所述,无论是HEAD还是branch,都只是引用。Reference+node是Git分布式分发的关键。与rebase相比,merge更有优势。清晰的时间历史,rebase会让提交更线性。它应该首先使用。移动HEAD可以查看每次提交对应的代码。clone或fetch会将远程仓库的所有提交和引用保存到本地。pull的本质其实就是fetch+merge,也可以加上--rebase通过rebase合并最近的热点文章推荐:1.1,000+Java面试题及答案(2022最新版)2.惊艳!Java协程来了。..3.SpringBoot2.x教程,太全面了!4.不要用爆破爆满画面,试试装饰者模式,这才是优雅的方式!!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!
