在我们的认知范围Git和SVN都是对于代码托管的工具,那么这两者又有什么不同呢?
Git
是世界上先进的「分布式的版本控制系统」,而SVN
是「集中式的版本控制系统」,SVN对于版本的管理集中于中央服务器中,而Git对于版本的管理可以在本地。
SVN管理的模式从SVN服务器中拉取代码,然后开始自己的开发,开发完后再向SVN服务器提交代码,所以集中式的版本管理,需要联网才能进行,一旦没网就没办法向SVN服务器提交代码。
而Git是分布式的版本管理,每个开发者的本地都会有完整的版本库,不需要来联网,也能进行版本的管理和代码的提交,每个开发者都可以再本地进行提交代码、查看版本、切换分支等操作。
所以相对于SVN来说Git的存储也会相对比较占用空间,但是以空间换来了Git对版本管理的高效,不得不说是一种高明的策略。
Git Bash Here
就是我们用来敲命令的窗口,打开它就可以敲关于Git的命令进行进行操作。
Windows的Git下载地址:,在这里下载最新版的进行安装即可。
下面我们来说一下Git再Linux的安装过程,要在安装Git其实也非常简单,可以直接使用yum源进行安装,一句命令就搞定了:
安装完Git后就开始对Git进行配置操作,配置自己用户名和Email,配置的命令如下:
配置完信息后,就可以「创建目录,并且初始化自己的本地仓库」了:
我这里已经初始化过了,初始化后会默认在主干上(master),这里为了测试各种Git的各种命令使用本地的Git仓库于github进行关联。
本地仓库与Github关联
执行git clone
就会生成一份副本,在本地仓库和工作区都会同步副本,具体的原理图如下所示:
从上面的图中我们可以到,代码可以在不同level之间移动,高level到低level,或者逆向低level到高level,也可以跨level之间移动。
Git中代码从低level到高leve的移动主要依靠以下命令:
-
git add .
:文件添加进暂存区。
-
git commit -m "提交信息"
:文件添加进本地仓库,-m参数改为-am可以直接推向本地仓库。
-
git push
:文件推向远程仓库。
运行git commit -a
相当于运行git add
把所有文件加入暂存区,然后再运行git commit
把文件提交本地仓库。
那么从高level向低level移动代码的命令如下:
-
git pull
:从远程仓库拉取代码到本地。
-
git reset --files
:用本地仓库覆盖暂存区中修改,也就是覆盖最后一次git add的内容。
-
git checkout --files
:把文件从暂存区复制到工作区,用于放弃本地的修改。
下面我用自己本地与github的操作测试上面的命令,加深对上面的命令的理解和使用,当我在本地新建一个github仓库中没有的文件:
可以看到文件的显示Untracked files:
未被追踪的文件,「表示该文件未被git追踪管理」。
新添加的文件可以通过「git add添加到在暂存区」,「这样文件就能够被git进行追踪」,此时再使用git status
查看文件时,就可以看到两个文件已经是以new file的形式进行显示:
若是你想撤销提交到暂存区的内容,使用git reset,可以撤销向暂存区新添加的文件:
也可以在使用命令:git reset --hard HEAD^
,表示回退上一个版本,「在Git中HEAD表示当前版本,HEAD^表示上一个版本」,若是有多个版本,这样表示就不方便了,可以使用HEAD~10
,表示版本的次数。
commit
的本质就是:「每次Git都会用暂存区的文件创建一个新的提交,把当前的分支指向新的提交节点,这样就完成了一次新的提交」:
若是HEAD
指针指向的是bran
分支,那么新的节点就会成为jh509
的子节点,并且形成新的分支:
当你再次检查你的代码的时候就会回到了id为5567a版本,在Git的版本回退原理中,Git的内部有一个指当当前版本的HEAD指针,只要从当前版本指回去就行了,所以Git版本的回退是特别快的,只需要移动指针,实现的原理图如下所示:
丢弃工作区的修改使用:git checkout -- file
命令,这条命令中的--files是不能漏的,若是只是git checkout
就表示切换另一条分支的命令了。
在我的本地我直接修改:README.md文件,然后使用git status进行查看,他表示文件处于modified状态:
此时的README.md文件是还没有被添加进暂存区的,可以直接使用以下命令,撤销掉工作区的修改:
若是已经添加到暂存区了,就是用以下的命令进行回撤:
上面也演示了git reset
命令,它既可以回退版本,又可以把暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本。
当你提交了修改后,可以使用git diff
查看两次提交之间的变动,它的本质就是「任意比较两个仓库之间的差异」:
在工作区直接使用rm fileName,这个操作和linux的命令一样,若是文件已经提交版本库,从版本库中删除文件可以使用git rm命令进行删除,然后提交:
若是删除错了,可以使用git checkout -- README.md
进行恢复,其原理就是使用版本库的文件替换工作区的文件。
在团队中集体使用Git的时候,每个人都提交自己的代码最后合并到主干,总有会push失败的时候,因为push的本质:「就是用你本地仓库的commit记录去覆盖远程仓库的commit记录」。
但是别人提交了一些代码,而你本地并没有这些代码,这样代码就会被覆盖,导致别人的commit的记录就不存在,这个是绝对不允许的。
所以,每次push的时候Git就会检查,若是存在这种情况就是push失败,只要先git pull一下,将本地仓库与远程仓库先合并一下,最后push就可以成功了,若是文件中已经存在在冲突代码,只要打开文件重新解决一下冲突即可。
Git中比较最重要的一点就是分支的概念,有了分支就有了合并和衍合的操作,「合并」和「衍合」能够「有效的对代码版本的管理」。
Git的初始化中有一条默认的主分支叫做master
,每一次的提交都会串成一条时间线,这就是一条分支,当前分支由HEAD
指针指向:
当每次发生代码提交的时候,当前指向就会向前形成一个新的版本,假如再创建一个新的分支bran,并且当前的提交指向新的分支,这样新的分支随着时间的推移就会形成许多版本:
当新分支开发完后,提交仓库,并合并到主干master,最后删除bran分支,这样就完成了一次个人的开发:
所以,假如主分支上只建立一条分支的话,分支的合并是非常快速的,只需要移动master分支到当前提交,然后将HEAD指针指向master,最后删除bran分支就完成了。
但是,事实上并不是这样的,在一个多人协作的开发团队中,往往每个人都会建立自己的分支,有自己的提交,最后合并到主干,当自己提交的时候,远程仓库代码就会存在自己本地仓库并未有的代码,这样就会导致push失败。
例如:程序员Tom和Jerry同时迁出代码,他们的初始代码分支都如下图所示:
当Tom开发自己的业务模块,提交代码并且合并到主干后,远程主干分支如下图所示:
「远程仓库master已经不再指向gs234,而是新生成了一个版本dfd453,作为当前指向的版本」。
与此同时,Jerry的本地也同时开发完自己的模块后,分支如下图所示:
在Jerry的本地环境中,他的「本地仓库master还是指向gs234」,Jerry在自己新建立的一个分支bran
中进行开发,开发完后合并分支,最后master
就会指向ed489
。
当Jerry再次提交代码,Git就会检查远程仓库与Jerry的本地仓库,进行对比后,发现远程仓库存在Jerry的本地仓库不存在的代码,就需要Jerry将远程仓库执行git pull
后,自行解决冲突。
上面说了分支基本原理,已经管理分支出现的问题,下面我们就来一步一步的深入操作分支的基本命令。
新建分支的实质:「就是新建立一个引用,指向当前提交,master就好比一个引用」;切换分支的实质:就是将HEAD
由指向原来的引用,重新指向要切换的分支的引用上:
当然上面创建分支并且合并分支的两条命令可以合并成一条命令:git checkout -b <分支名字>
当切换分之后,每次commit提交代码时HEAD指针就会跟随着新的bran分支移动,形成bran分支上的每一个版本:
假如,在新的bran分支上开发到某一个版本,再次切换回master分支进行开发就会形成分叉:
当分支创建好了,你可以通过:git branch
,来查看自己本地的分支情况:
分支前面带有*号的表示当前的分支,查看分之后,你就可以很清楚的知道自己要checkout哪条分支了。
开发玩自己模块后,后面就会在自己本地进行合并分支,合并分支的命令:git merge <分支名字>
,它表示「合并指定的分支到当前分支」,比如:当前分支为master,执行:git merge bran
,表示合并bran分支到当前master分支上。
分支合并也会有失败的情况,当你的两条分支都修改的相同的文件,这时候Git就无法判断你要保留哪一个修改,就会出现merge冲突。
例如:我先在master
分支修改README.md文件,然后提交本地仓库:
然后切换回分支dev,再次修改README.md文件,再次提交
最后进行合并分支,此时在你两次修改的README.md文件中就会出现两次修改的冲突代码:
因为你两次修改同一文件的操作,合并后Git并不知道你要保留哪一次的操作,所以它就会将这个决定交给你自己决定,它只告诉你文件中哪里的代码冲突了,具体怎么改就由你自己去弄。
最后是删除自己新建的分支,通过:git branch -d <分支名字>
,进行删除分支,假如分支删除不了,可以通过:git branch -D <分支名字>
,强制删除分支:
Git中删除分支的实质:dev只是一个分支的引用,所以删除分支也就是删除这个引用,并不会删除任何conmit,所以删除操作也是非常高效的。
假如一条分支commit的引用被删除,那么这条分支的就没有任何引用指向,这样就会找不到这条分支,最后就会被Git回收机制回收。
在多人协作的团队下,你可能要随时查看远程仓库的情况,可以通过:git remote,进行查看,加上-v参数可以查看远程仓库的详细情况。
分支的推送到远程上一节已经提过,使用git push命令就可以进行分支的推送,命令后面加上分支的命令,表示具体推送哪条分支:
分支的拉取使用git pull命令,这条命令相当于以下两条命令:
但是一般实际工作中,都可能会直接使用git pull命令:
在合并分支的时候,Git会以快速合并的模式进行合并(Fast forward),但是这种模式删除分支后,会丢失分支的信息。
Git中还可以以「普通模式」进行合并,在原来git merge命令后面加上--no-ff参数即可,合并的命令如下:
在开发中,若是某一时刻你想把当前的改动临时进行存放起来,可以使用git stash
命令,它表示将改动的文件存储到一个独立的存储区域,并不会被提交,当再次需要的时候可以随时取出来。
这里要注意的是:「git stash的是改动的文件,也就是被Git追踪的文件,新添加的文件并没有被Git追踪,所以git stash并不会stash」。
git stash命令也可以加上save命令后面再加上备注信息,方便查看:
git stash
成功后「本地的工作目录的代码会和本地仓库一样」,git stash
后可以通过git stash list
命令查看之前stash的历史记录,当再次需要将改动的文件取出来时候,可以通过以下命令:
git stash pop
表示「弹出第一个被stash的记录,并且该stash会从历史记录中删除」;也可以使用git stash apply
命令「弹出stash,但是这条命令stash任然会保存在stash历史记录中」,你也可以通过:git stash drop
命令来删除。
这一篇就只讲解了Git的分支原理以及Git的临时存取操作,限于篇幅,我们今天就到这里,我们下一期继续图解Git操作,我们下一期再见。
这一篇我们继续图解Git,上两篇原创图解了Git的基本操作,有兴趣的可以看一看[]和[]。
这一篇写完基本Git的操作就图解完了,如果想深入了解Git,这里可以推荐一些Git的硬核书籍:【精通Git】、【GitHub入门与实践】、【Git权威指南】、【Git版本控制管理】、【GitHub实践】。
这些都是一些关于Git的比较好的书籍,有兴趣的可以可看一看,网上也有很多电子书。闲话不多说,下面就开始我们的正题。
Git查看日志前面有提到过可以通过git log
命令进行查看:
git log
可以查看你「提交的时间、提交的作者、以及提交的id」都可以查到,如果你觉得查询的信息太多,可以加上参数--pretty=oneline
,只会显示版本号和提交时的备注信息。
如果你想查最近的几条历史记录,可以通过加参数"-n
"的形式制定查询几条记录,历史记录是「按照操作的时间」进行排序的:
还可以通过加参数" --graph
",以图形化的形式展示历史记录,方便与查看历史记录与分支的关系:
还可以加参数"-p
",可以查看每一个commit操作更改的文件的哪一行,加参数"-stat
"查看哪些文件改动了,进行简要的统计。更加详细的git log
参数可以查看命令帮助。
Git查看历史记录的另一个命令是git reflog
,它可以查看「所有分支的所有操作记录,包括已经删除的commit记录和reset记录」。
分支管理中有「合并」和「衍合」操作,合并操作在在第二年篇的分支章节已经详细讲解过了,就来讲解一下衍合操作:git rebase
操作。
假如有两位开发人员Tom和Jerry,Tom和Jerry都把远程的master
分支签出到本地,此时当前的Tom和Jerry本地都是只有一条master
分支:
此时Tom本地仓库和远程仓库的分支保持一致,分支如下图所示:
Tom在自己的分支branch
上开发自己的模块,假如开发期间Tom进行了「两次」的提交,最后Tom本地的分支形成如下所示:
也可以通过git log
查看两次提交的记录:
若是,此时Tom开发人员准备把自己的branch的分支推向远程仓库,但是,Jerry在此之前已经在master提交了自己的开发代码,所以master分支相比之前记录,已经向前推进了一个版本。
所以此时Tom想提交,必须先更新一下自己的本地分支:
Tom中通过git log
命令可以查看到Jerry的提交记录情况,说明此时分支已经与远程仓库同步:
此时Tom更新分之后,本地的分支情况如下所致,相比原来master指向c1,现在向前推进了一个版本指向c4:
此时,Tom必须重新合并分支进行提交,把branch的代码合并到master分支上,现在Tom可以有两种方式:
(1)若是使用第一种方法直接在master分支上执行git merge
命令,「Git会把master分支上最新的提交c4的内容和branch分支上最新的提交c3 合并后生成一个新的提交点c5」:
上面实在没有冲突的前提下,若是有冲突,则解决冲突:
此时就完成了master和branch分支的合并。
(2)若是使用第二个方法,先master也需要拉取到最新版本,然后是切换到branch分支,这个也就是要切换到rebase
的分支,这里指的是branch分支。
通过测试可以发现,原来branch分支上重新commit的节点c4在rebase到master分之后hash发生了改变,并且提交的内容被复制保留,从而使得master分支整个呈现线性的commit记录,而不是直接git merge后的分叉记录。
注意:「一般不是在branch进行rebase主分支master的提交,因为会导致master的新提交在本地丢失,这样有可能会导致本地仓库与远程仓库发生冲突,从而无法push操作」。
「总结」:「git merge会将两个分支的最新提交点进行一次合并,形成一个新的提交点,最终形成树状的提交记录」,但是有些人并不是喜欢merge,觉得merge之后出现的分叉会难以管理,那么可以选择rebase操作来替代merge。
「git rebase操作是将要rebase的分支最新提交点作为新的基础点,将当前执行git rebase master的分支的新commit点重新生成commit hash值,rebase完后再次切换到另一条分支进行合并,就可以保证线性的commit的记录」。
「最后只选择merge还是rebase取决于个人和时机情况,假如你想提交记录呈现线性整洁那么选择rebase,否则选择merge,实际情况也有可能是这样的,每个人本地开发,可能会提交非常的多次,有些提交可能是修一些简单的bug,那么最后的提交只想做一次完整、正确的提交,那么也可以使用rebase。」
Git中使用的标签有两种类型:「轻量级的(lightweight)和含附注的(annotated)」。轻量级标签只需在git tag
后加上标签的名字,就可以添加标签。
标签管理作为开发人员可能很少使用,可以作为了解,「tag是针对Git中某一时间某一版本打上标签」,tag的使用命令也是非常的简单。
新建一个标签,默认是在HEAD
新建,可以指定commit id
新建,具体命令如下所示:
删除标签若是标签没有推向远程仓库,直接使用以下命令删除:
若是标签已经推向远程仓库,先删除本地,再删除远程仓库的标签:
git tag的方式是查看所有的标签,git show <标签名>的方式是查看每个特定的标签
推送标签也是分两种情况,一种是指定标签的推送,另一种是推送所有标签。
代码错误提交了怎么办,重新再一次提交一个版本呗,这个可能是很多人的解决方案,当然Git也是有提供自己的解决方案的命令。
第一种就是再次将修改文件然后git add .
到暂存区,最后git commit --amend
提交修改 ,它的原理图如下所示:
这种方法只能修改当前HEAD,也就是最新的提交,那么要修改是倒数第二个或者倒数第三个的提交呢?
这时候就要使用rebase -i
(「交互式rebase」)进行操作了,这个命令是指定commit链中哪一个commit需要修改。
若是想直接丢弃最新的commit的修改,则直接使用命令:git reset --hard HEAD^
。他表示当前commit往前移动一次。
好了图解Git操作基本讲解完了,其他的一些细节操作基本都是在基本操作的基础上加参数,详细的参数大家可以参考官网或者相关的书籍。
在公司的实际应用这三篇图解Git操作基本可以应付了,上面说的交互式操作,基本没用过,只做大致的了解,但是之前在面试华为的时候有被问到Git的交互操作。
最后,帮助粉丝Git的进阶,给粉丝送福利,送出两本书 「《Git入门到精通》」,获取的形式本文的「留言区前两名的粉丝」可以获得,活动时间到10/8 18:00 -10/10 12:00。