Git核心功能探秘——续篇

来源:互联网 发布:黑暗之光元神升阶数据 编辑:程序博客网 时间:2024/05/29 19:19

        在上篇“Git核心功能探秘”一文中,主要对Git的结构原理,与传统版本工具的异同,以及主要功能含义进行了基本阐述。刚刚接触Git的使用者通过上文应该可以上手进行实践,但是随着项目的深入,会遇到各种各样的问题,如Git版本恢复、冲突与解决等。要想更好的解决这些问题,必须对Git内部组织和各个功能的细节进一步分析阐述。因此本文中我们将更进一步,探索这些重要细节。

Git内部组织探析

        Git的内部组织从空间上由工作区、索引和版本库组成,版本库内的逻辑结构则是由引用、分支和快照组成。首先解释空间组织中涉及的三个概念:

  • WorkingDirectory/WorkingTree工作目录是日常编辑文件时的目录,工作目录中的文件开始时默认处于未被跟踪状态(UntrackedFile)。

  • Index/StagingAreaGit索引是处于工作目录和Git版本库之间的区域,可以理解为暂存区。这个区域存储着工作区中纳入Git控制的文件中的修改,需要进行commit进入版本库

  • GitRepositoryGit版本库,保存着每一个分支的每一个commit记录。

工作区中的文件可能有如下四种:

  • Untrackedfile未被追踪的文件,文件并未被Git管理;

  • Changedbut not updated文件被修改但未被暂存;

  • Changesto be commited文件的更新已被暂存但是没有被提交;

  • Clean文件自上次提交以来,没有被修改。

        文件的后三种状态都属于已经被Git所管理,即Frackedfile

        下面用一个简单示例把文件的状态与Git三个空间组织之间的关系串连起来。

        假设我们在工作目录下新建了一个文件a.txt,这时新文件处于Untracked状态,没有被Git管理。我们打开a.txt编辑一些内容并保存,其状态仍然是Untracked

        如果想把a.txt纳入Git管理,则需要执行gitadd a.txt命令,这时文件被放入Index,并处于Changesto be commited状态,由于a.txt是一个新文件,Index中的Changes就是a.txt文件的全部内容。

        这时如果我们再编辑工作区中的a.txt文件,a.txt将呈现两种不同的状态:之前在index中的部分为Changesto be commited;当前更该的内容不会自动添加到index中,这部分内容为Changedbut not updated。这时需要再次执行gitadd命令才能使a.txt的自上次提交以来的全部修改保存到indexa.txt文件将再次呈现一种状态,即Changesto be commited

        如果需要把当前的a.txt记录下来,则需要执行gitcommit,这时a.txt的版本将被记录到Git库中,文件这时处于Clean状态。

        了解完了Git的空间组织,再来了解一下GitRepository的逻辑结构,逻辑结构的主要概念包括headcommitbranch。如果你学过计算机专业中的数据结构,那么这些概念应该很容易就能理清,因为逻辑结构就是链表和树。

        首先,Git版本库中的一个commit是工作区的一次快照,是当前工作区文件状态的保存。每一个commit都是在前一次commit的基础上作出的,因此commit之间形成了链表结构,而一个commit链也称为一个branch

        一个commit链可以产生新的分支,新分支的commit基于分支点的commit的,也就形成了树形的结构。在Git版本库中可以有多个分支,通过数据结构的知识我们知道每两个分支都有一个交汇点,并且所有分支都是基于第一次的commit,也就是root节点。用户可以随意切换不同的分支,但是当前只能工作在一个分支上,也就是当前分支,当前的所有操作,如mergecommit等也都是在当前分支上进行的。

        而Githead实际上就是一个指针,指向当前分支最新的commit,这和数据结构中链表的head概念如出一辙。只不过随着commit的增加,head从头向后移动,并始终指向最新commit,也就是链表尾部,因此我认为称为Gittail更合适(算是给Git设计者提的一个小建议吧)。

        深入理解Git的空间和逻辑结构还是非常有必要的,因为很多命令的高级选项与此相关,而且很多问题的解决也需要建立在对Git内部的深入理解上。下面就进一步阐述Git常用命令和冲突问题的解决。

checkoutresetrevert

        Git的这三个命令都有恢复到以前某个版本的作用,但是含义和用途还是有差别的,并且很容易被混用。Gitcheck out有两种作用:

1、将指定分支作为当前分支,或者新建分支并将其做为当前分支。同时,工作区和index的内容也将和指定分支最新版本一致。

2、将当前分支的指定版本(commit)检出,也就是把工作区和index的指定文件和所有文件更改成当前分支的指定版本。

        从上面的两个功能来看,Checkout最大的特点是对于Git版本库没有破坏操作,也就是说既不会在Git版本库中增加新版本,也不会删除任何之前的版本。只是Checkout会把工作区和index的指定内容更该成和Git版本库的某个版本一致的状态,以后工作区和index的内容当然也可以用checkout回到当前分支的最新版本。

        唯一需要注意的是如果工作区和index有没有commit的内容是,这时checkout之前的版本,这些没有提交的更新会丢失。不过如果使用EclipseGit插件进行checkout时会提示你先把改动提交。

Gitreset命令是对Git版本库的破坏性操作,它会将Git版本库的版本回退到指定版本,而指定版本之后的版本将被删除。Gitreset最重要的三个参数是-mixed-soft-hard,区分它们的关键就在于对前面Git空间结构的理解。

  • -mixed:默认参数,回退到指定版本时回退Git版本库和Index,但是不会回退工作区。这时工作区中某些文件可能处于Changedbut not updated状态;

  • -soft:回退到指定版本时只回退Git版本库,但不改变index和工作区文件。这时index和工作区的文件一致,但是会有某些文件处于Changesto be commited状态。

  • -hard回退到指定版本时同时回退Git版本库、index和工作区。这是最彻底的一个命令,操作之后三个空间内容将保持一致,工作区所有文件处于clean状态。

        这里有一个很实际的问题,如果reset-hard是一次误操作,先在想把Git版本库中删除的版本找回来怎么办?这时就需要GitReflog了,GitReflog记录了Git版本中所有commit相关的操作,想恢复的话找到相应的版本操作,checkout删除的版本,再gitaddcommit就行了。(貌似gitreset到被删除的版本就可以了)。

        Gitrevertreset很类似,都是对Git版本库中版本的修改。区别是Gitrevert可以撤销某次commit,但是之后和之前的commit都会保留;而Gitreset是回退,之后的所有commit都不会被保留。

冲突解决

        冲突是使用Git进行协同开发时经常遇到的问题,因此必须知道冲突如何解决,否则Git的使用会很低效。顺便说一句,有些人认为频繁在pull时出现冲突而认为Git很垃圾,我认为从哲学角度看这是犯了典型的形而上学的错误。冲突时协调开发时客观存在的,不管你用不用Git,冲突就在那里,即使用其他的版本工具也一样。如果什么版本工具都不用,情况将更糟糕,你将感觉不到冲突,但不是因为冲突不存在,而是新文件粗暴的把相同的旧文件覆盖了,你在旧文件上做的修改都将丢失,这是数据库多事务中典型的丢失修改问题。

        现在言归正传,首先讨论一些冲突是怎样产生的。当你成功的在远程节点进行了pull操作,并在新版本基础上对某文件a.txt做了修改,然后commit。这时另一个人也对a.txt文件做了修改并commit,而且他比你抢先一步将commitpush到了远程主版本库。这时你在pull的时候就会产生冲突,因为a.txt文件是在同一个版本上作出的,你必须决定如何修改a.txt,是保留你的修改?还是保留别人的修改?还是重新做新的修改?

        有可能发生冲突的操作包括pullmergerebaseapplypatch。如果使用EclipseGit插件进行管理,发生冲突时工程文件的相关文件和目录将会有一个大红点。

        发生冲突时,相关操作并没有完成。例如merge发生冲突时,Git当前状态变为conflictmerge之后的版本并没有commitGit版本库中,Git版本库中的版本并没有变化。这时需要注意的是,merge时成功merge没有冲突的文件更新处于index中,而发生冲突的文件在工作区中,并没有在index中。因此,在解决完文件的冲突后,需要执行gitadd将更新加入index中,这样Eclipse工程中冲突文件的那个大红点才会消失。

        下面关注如何解决冲突。每一个冲突的文件,也就是带有大红点的文件内容一般都会有“<<<<master/head”“====”和“<<<<remote/master/head”的标记,其中“<<<<master/head”和“====”之间的内容是你的更改,而“====”和“<<<<remote/master/head”之间的内容是其他人的更改。这时如何去修改这个文件,要保留哪些东西就要和别人进行协商了,或者等着teamleader来仲裁。修改完冲突的文件后,执行gitadd命令,冲突文件的大红点消失(为何执行gitadd上一段已经说明)。

        当所有的冲突都修改好,并执行gitadd将这些冲突修改放到index中之后,工程文件就应该没有大红点了,这时可以执行commit操作,将本次merge后的信息保存到版本库,本次merge过程也就成功结束了。

结论

        本文讨论并解决了困扰Git使用者的两个大问题,版本撤销和冲突解决。从问题的解决中可以体会到要想真正搞明白这些问题和Git的命令之间的含义区别,对Git内部空间结构和逻辑结构的深入理解至关重要。反过来,真正理解了Git的内部机制后这些问题也就迎刃而解了。因此,本文更多想表达的是一种启示,在今后遇到类似问题时不要着急,去研究一下相关的内部机理,再寻找解决方法,而不是直接网上道听途说一番,因为后者如果不是对症下药的话很可能引入新的问题,导致混乱。

参考资料

[1]Scott Chacon. Pro Git. Apress, 2009.

[2]Andrew Burgess. Geting Good with Git. Rockable Press, 2010.

[3]Git Reference.http://git-scm.com/docs.


原创粉丝点击