前文中,我们已经初步的使用了git,我们通过创建"提交"的方式将某个状态保存下来,"提交"就相当于我们手动管理版本时所创建的"副本",但是,"提交"其实比"副本"高明的多,那么到底高明在哪里呢?我们一起来看看。
首先,来说说手动进行版本管理时所创建的副本。 假设,我的代码都放在一个名为project的目录中,现在project目录中有很多文件,总共占用磁盘空间2M,现在,我想要修改其中的一些代码文件,为了整个项目能够随时恢复到当前状态,我需要手动的创建一个副本,于是,我将整个项目目录拷贝了一份,这个副本也占据了2M磁盘空间,于是我开始了修改工作,修改工作完成后,我觉得修改后的状态也需要保存一下,以便随时回到这个状态,于是,我又创建了一个副本,这个副本也占据2M磁盘空间,也就是说,我每次创建副本,都是上述过程的重复,无论我修改了多少代码,即使只修改了一点点,我也需要将整个目录复制一份,以便保证目录中的所有文件在某一时刻的一致性,换句话说就是,即使两个版本之间的差异只有1k,但是也需要牺牲约2M的空间,来实现版本管理。
那么git是怎样实现版本管理的呢?git使用的方法,比我们高明的多,事实上,git只会对修改的部分创建副本,而不会对整个目录创建副本,那git是怎么做的呢?如果想要了解git是怎么做的,则需要从git对象说起,"git对象"的概念并不难理解,请坚持看完下文,看完了,自然就理解了。
当我们使用git进行版本管理时,git会将我们的文件和目录结构转化成git方便操作的数据,也就是说,git会将我们的文件和目录转化成一种叫做"git对象"的东西,然后再对这些"git对象"进行管理,从而实现版本管理的目的,这些git对象存放在git的对象库中。
我们眼中的文件会被git转化成"块"(blob) 我们眼中的目录会被git转化成"树"(tree) 我们眼中的状态会被git转化成"提交"(commit)
blob、tree、commit都是git对象,是三种不同类型的git对象 一个blob就是由一个文件转换而来,blob对象中只会存储文件的数据,而不会存储文件的元数据。 一个tree就是由一个目录转化而来,tree对象中只会存储一层目录的信息,它只存储它的直接文件和直接子目录的信息,但是子目录中的内容它并不会保存。 一个commit就是一个我们所创建的提交,它指向了一个tree,这个tree保存了某一时刻项目根目录中的直接文件信息和直接目录信息,也就是说,这个tree会指向直接文件的blob对象,并且指向直接子目录的tree对象,子目录的tree对象又指向了子目录中直接文件的blob,以及子目录的直接子目录的tree,依此类推。
每个git对象都有一个"身份证号",这个身份证号是一个哈希码,这个哈希码通过SHA1算法得出,如果git对象的内容相同,那么他们的哈希码就是相同的,如果git对象的内容不同,那么他们的哈希码必定不同(通常来说,SHA1算法能够保证内容不同时,得到的哈希码必定不同,不过,理论上来说,即使内容不同,也有可能产生相同的哈希码,不过几率非常之小,我们可以忽略这种可能性),一个git对象的哈希码通常长成如下模样: 875925683e755d94e26a2dc1a1bc4c645a91acbe 它是一个40位的十六进制数。 刚才提到,每个git对象都有一个这样的哈希码,所以,每个"提交"(commit)也有一个这样的哈希码,在后文中,我们会使用提交的哈希码来表示某个提交,不过由于这个哈希码比较长,所以通常情况下,我们只会使用哈希码的前几位来表示一个提交,只要这个哈希码的前几位与别的哈希码的前几位不同,能体现出唯一性,我们就能用这个哈希码的前几位来表示这个提交,比如,刚才示例的哈希码如下 875925683e755d94e26a2dc1a1bc4c645a91acbe 我们可以使用8759256来表示这个哈希码。
只看上面一段描述,可能不太容易理解,我们来看张图,用图来描述上述内容似乎更加容易理解,下图模仿于 [Git版本控制管理]一书,下图中的圆形代表commit(即前文中的"小圆球"),三角形代表tree(由目录转化成的git对象),长方形代表blob(由文件转化成的git对象)。假设,第一次提交之前,目录中只有两个文件,file1和file2,file1的内容为f1,file2的内容为f2,那么当第一个提交创建以后,git的对象库中会存在如下图的git对象
也就是说 ,当我们创建第一个提交以后,项目当时的状态已经被转化成了上图中的git对象,我们创建的第一个提交的哈希为8759256,它指向一个tree,这个tree就是当时根目录的状态,这个tree的哈希为e890df4,从上图可以看出,当时的根目录中只有两个文件,也就是两个blob,这个tree指向了这两个blob,这两个blob就是由file1和file2转化而来的。
如果此时,我们修改了file2,我们将file2的内容从f2修改成f22,并且在根目录中创建一个新的子目录dir1,在dir1中又添加了一个文件d1file1,d1file1的内容是df1,但是我们并没有对file1进行任何修改,那么,当我们再次提交以后,git对象库中会存在如下对象。
如上图所示,我们修改了file2,将其内容从f2修改成了f22,当第二个提交创建以后,git会将file2的新状态转化成一个新的blob对象,file2之前的状态对应的blob对象仍然保存在git对象库中,并且被初始提交引用,以便我们随时能够通过初始提交找到file2当时的状态,file2新的状态被新的提交引用,我们并没有修改file1,也就是说,file1的状态一直没有发生改变,所以,新的提交只是通过tree对象指向了之前file1对应的blob,由于我们在根目录中创建了一个子目录dir1,所以,在新的根目录的tree对象中,也包含了它的直接子目录信息,并且指向了新子目录对应的tree对象,子目录tree对象中又保存了自己目录中的信息,也就是d1file1文件对应的blob对象。
看完上述过程,我们回到最初的问题,当我们手动创建副本时,为了保存项目中所有文件在某一个时刻的状态的关联性和一致性,我们需要对整个项目(所有文件)创建副本,这种做法就会导致之前描述的问题发生,即使两个副本之间的差异只有1k,也需要牺牲2M的磁盘空间,每次创建副本,无论改动的大小,都会牺牲整个项目大小的磁盘空间,这样在频繁创建副本的情况下,是非常不划算的,但是git的做法就高明的多,它只会对改动的文件创建副本,就像上例中的file2,当file2的状态发生改变时,git才会对file2的新状态创建新的blob对象。 file1的状态没有发生改变,git就不会对file1创建副本,也就是说,在两个commit中(在整个项目的两个状态中)file1的状态是相同的,于是git并没有对file1重复的创建blob,而是通过引用的方式,指向了file1对应的blob,即两个副本复用了同一个file1的状态,所以,当我们使用git进行版本管理时,只会牺牲最小的磁盘空间,来实现版本管理。
我对git的理解似乎加深了,因为我明白了,一个commit就代表项目的一个状态(相当于手动创建的副本),一个commit背后是一堆git对象,git将这些git对象巧妙的组织在了一起,从而实现了版本管理的目的。
|