0。前言Git作为世界上最强大的代码管理工具,相信大家都不陌生,但据我所知,大量的人停留在clone,commit,pull,push...的阶段,不懂rebase还敢用merge吗?遇到版本回滚就抓瞎?不要问我怎么知道的,就问:“我以前就是这样的~~”。针对这些问题,今天分享一下自己这几年对Git的认知和理解,尽可能从本质上讲解Git,一步步帮助大家理解Git的底层原理。相信看完本文后,你能以不一样的姿态,更加风骚地使用各种Git命令。目录1.基本概念1.1Git的优点1.2文件状态1.3提交节点1.4HEAD1.5远程仓库2.分支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只有一个。通常根据功能或版本建立不同的分支。一定要记住,不管是HEAD还是分支,都只是引用,量级很轻。通过命令添加一个文件到暂存区:gitaddfilepath将所有文件添加到暂存区:gitadd。同时Git也提供了撤销工作区和暂存区的命令来撤销工作区的改变:gitcheckout--文件名被清空暂存区:gitresetHEAD文件名提交:添加改变的文件后到暂存区,即可提交。提交后,会生成一个新的提交节点。具体命令如下:gitcommit-m"本节点的描述信息"3.2branch相关分支的创建创建一个分支后,该分支会指向与HEAD相同的节点。通俗一点就是新建的分支会指向HEAD指向的地方。命令如下:gitbranchbranchnameswitchbranch切换分支时,HEAD会默认指向当前分支,即HEAD间接指向当前分支指向的节点。gitcheckout分支名也可以创建分支并立即切换。命令如下:gitcheckout-b分支名删除分支。应该在任务结束后移除。比如上面说的,开一个单独的分支来完成某个功能。当该功能合并到主分支时,应及时删除该分支。删除命令如下:gitbranch-d分支名3.3Merge有关合并的命令是最难掌握的,也是最重要的。常用的合并命令大概有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时,C4节点在C3后面复制了一份,即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//也可以直接离开分支指向当前节点gitcheckout--detach由于hash值是一长串乱码,使用hash很麻烦value在实际运行中用来分隔HEAD,所以Git也提供了HEAD基于一个特殊的位置(branch/HEAD)直接指向前一个或前N个节点的command,也就是相对引用,如下://HEAD分隔并指向上一个节点gitcheckout分支名/HEAD^//HEAD分隔并指向前N个节点gitcheckout分支名~N分隔HEAD指向该节点有什么用?例如:如果开发过程中发现之前的提交有问题,此时可以将HEAD指向对应的节点,修改后再提交。这时候你肯定不想生成新的节点,只需要在提交时加上--amend即可,具体命令如下:gitcommit--amendrollback回滚场景在平时开发中比较常见。比如你写了很多代码提交了,后来发现写的有问题,那么你如果想把代码返回到之前提交的时候,这种场景可以通过reset来解决。具体命令如下://ReturnNsubmissionsgitresetHEAD~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命令可以从远程仓库中的引用中拉取代码。具体命令如下:gitpullremotebranchname其实pull的本质就是fetch+merge。首先将远程仓库的状态全部更新到本地,然后再次Merge。合并完成后,本地分支会指向最新的节点。另外,pull命令也可以通过rebase进行合并。具体命令如下:Push可能会失败,因为可能有冲突,所以往往在push之前pull,如果有冲突,就在本地解决。推送成功后,本地远程分支引用将更新为指向与本地分支相同的节点。综上所述,无论是HEAD还是分支,都只是引用。Reference+node是Git分布的关键。Merge比rebase有更清晰的时间历史,rebase会让提交更线性。应该先使用,通过移动HEAD可以查看每次提交对应的代码。clone或fetch会将远程仓库的所有提交和引用保存到本地。pull的本质其实就是fetch+merge。也可以加上--rebase通过rebase合并
