前言
工作中使用git是从两年前开始的,之前一直add -> commit ->push
常规操作,真正在工作中使用之后才渐渐理解了git的强大,这种理解是建立在不断解决问题的基础上的,不断的处理遇到的问题,就像升级打怪一样,对git的理解也越来越全面。因为在使用git之前一直用svn作为版本控制工具,所以对git和svn的区别也有了自己的认识,关于两者的区别网上的文章一搜一大把,我就不重复了,我仅仅从自己的理解来描述下两者的不同。
git和svn
关于git和svn的区别,网上的文章确实很多,大多数会提到分布式、存储方式、版本号、完整性等方面,而我今天要写的区别是两者提交记录的结构。
既然作为版本控制工具,那么每次历史提交都必须可以追溯和回退,在svn中提交记录时线性的,以时间轴为参考基准,所有提交按照时间先后排列,因为svn记录必须提交到服务器才能生效,所有服务器相当于各个svn客户端的总控,各个svn提交到服务器时线性排列,且必须将本地文件状态更新成和服务器相同时才能修改提交。
正因为在svn中有服务器负责总控操作,所以能保证时间最新的提交记录就是整个svn最新的状态,提交记录不依赖客户端时间,完全由服务器时间进行排序。
在git中没有这样的总控服务器,虽然一般情况下每个代码库都会有统一的托管服务器,但是它的作用任何一个git客户端都能代替,因为git是可以离线提交的,托管服务器只是我们用来存储代码的地方,与svn服务器按时间排序的做法大不相同。
git的提交记录通常是一个树形结构,个别时候会变成有“起点”和“终点”的网状结构,在git中时间只具备参考意义,并不能决定提交记录的先后,如果你对这一点还心存怀疑,可能你是个svn的重度用户,一时还没理解git操作原理。
对于这个问题可以举个例子,操作同一个文件,在svn中2月13日修改一次,2月14日修改一次,那么2月15日看这个文件一定是2月14日修改后的状态;而在git中,同样是那个文件分别在2月13日和2月14日修改一次,2月15日文件的状态取决了两次修改是否在同一分支,以及合并时是怎样处理的,这种错位随着时间的延长和多分支的合并,往往对时间的依赖“微乎其微”,此时再也不能用时间来衡量提交的先后了。
如果一开始就是git,上面提到的这个问题还不太明显,但是用惯了svn再使用git,处理历史回溯问题时往往容易找错方向,经常通过时间过滤出来的内容并不是自己想要的,这一点在实际操作中需要注意。
git最重要的是什么
相信这个问题每个人都有自己的答案,有人认为是分布式,有人认为是切换分支很方便,而我的答案是 commit 的设计哲学
,我觉得这是git中的精髓,git中的commit就像一个链表中的元素,用来将自身和其他的commit串联到一起,形成branch
、tag
、HEAD
等等。
我们可以通过 git log
命令来看一条 commit:
$ git log -1
commit 7bf665f125a4771db095c83a7ad6ed46692cd314 (HEAD -> 6.0, tag: 6.0.6, origin/6.0)
Author: Oran Agra <oran@redislabs.com>
Date: Sun Jul 19 14:00:20 2020 +0300
Redis 6.0.6.
这条commit id 为 7bf665f125a4771db095c83a7ad6ed46692cd314
,这在整个库是惟一的,通过 git log
可以看到这次提交的时间、作者、简要说明等信息,那么这次提交和库是什么关系呢?
通过括号中的内容可以知道当前提交是这个库的6.0
分支,同时为标签6.0.6
,也与远端的6.0
分支同步。
使用 git cat-file
命令可以进一步查询这个commit的组织形式:
$ git cat-file -p HEAD
tree c3d4b2bcd934be7e4ed98edac5aa7e9c054503c3
parent a5696bdf4f2687ab45f633ccb7cdc4ee9c2f957d
author Oran Agra <oran@redislabs.com> 1595156420 +0300
committer Oran Agra <oran@redislabs.com> 1595268506 +0300
Redis 6.0.6.
可以发现这次提交包含了 tree c3d4b2bcd934be7e4ed98edac5aa7e9c054503c3
,同时它的父提交就是 parent a5696bdf4f2687ab45f633ccb7cdc4ee9c2f957d
,有了这两个id就可以递推出当前版本内容和这个历史记录。
通过 tree c3d4b2bcd934be7e4ed98edac5aa7e9c054503c3
可以递归找出当前版本中的所有文件:
$ git cat-file -p c3d4b2bcd934be7e4ed98edac5aa7e9c054503c3
040000 tree 6608d88fe6a7a25b137b869040103ab261310da4 .github
100644 blob e445fd2017bb0c13af2f40cd7f24afefdb603ade .gitignore
100644 blob 484aeb62186033d32e9a4bdf12434cb6b8c56fb5 00-RELEASENOTES
100644 blob 7af2593407805c308cc25739ac9c6520031de60f BUGS
100644 blob 000edbeaf0270bf3b9e457274ab092b02b176b84 CONTRIBUTING
100644 blob a381681a1c2524ed586c6a87dfeb9ccdf1e86ded COPYING
100644 blob 3083f1afd50c34e1139ab1577510a17e968b0ed4 INSTALL
100644 blob 3727894624fdabf72995e6f94998a2cad359f760 MANIFESTO
100644 blob e614ede891f2dd183a3ae41ea1ac3b63fe2e7634 Makefile
100644 blob 55537e01fe862dd200ebe1078033122facfc854e README.md
100644 blob 2d020d0ceb0ddc7fd0bb2a6185e57a9afd5aef79 TLS.md
040000 tree 43ccdd93a80b35e03160d9db34f1e844a62a74b4 deps
100644 blob 8c53f015a20934bdb41c77152fd32a557d719fae redis.conf
100755 blob ade1bd09a539ecd8dcdd09e59a658539dab9bce6 runtest
100755 blob 27829a5fe8afacf893fe9bafc4245971ce375d6c runtest-cluster
100755 blob f6cc0a2589dea0f95b77b226e54200a29b8237ae runtest-moduleapi
100755 blob 3fb1ef61561289b2bf8622e49645f66dab83eeea runtest-sentinel
100644 blob 4ca5e5f8fc5abe2938c66a6851bba0c90058620f sentinel.conf
040000 tree e3b3338a7c60eafb3d9c19d3784e2482beea1d4b src
040000 tree af5de133fa0a0da30fe487be40783ef9644fba6d tests
040000 tree 5a82556097d23f0c16a8e5432d464f2ab434fd2a utils
通过 parent a5696bdf4f2687ab45f633ccb7cdc4ee9c2f957d
可以找出上一次提交,进而递归找出所有的提交,要注意有些commit的parent不止一个:
$ git cat-file -p a5696bdf4f2687ab45f633ccb7cdc4ee9c2f957d
tree 1adcf548620c6134f7d5fd072c05b981d0f36118
parent e15528bf1da1f1232fd08801ad382c915be94662
author Oran Agra <oran@redislabs.com> 1595162001 +0300
committer Oran Agra <oran@redislabs.com> 1595268506 +0300
Run daily CI on PRs to release a branch
这个commit的设计真的很神奇,一个个commit串起来就是一个branch,本质来讲branch
只是commit的一个别名,包括HEAD
也是,而 tag
也是对commit的一个描述,在不加描述信息时和commit也是一样的。
$ git cat-file -p 6.0.6
tree c3d4b2bcd934be7e4ed98edac5aa7e9c054503c3
parent a5696bdf4f2687ab45f633ccb7cdc4ee9c2f957d
author Oran Agra <oran@redislabs.com> 1595156420 +0300
committer Oran Agra <oran@redislabs.com> 1595268506 +0300
Redis 6.0.6.
$ git cat-file -p HEAD
tree c3d4b2bcd934be7e4ed98edac5aa7e9c054503c3
parent a5696bdf4f2687ab45f633ccb7cdc4ee9c2f957d
author Oran Agra <oran@redislabs.com> 1595156420 +0300
committer Oran Agra <oran@redislabs.com> 1595268506 +0300
Redis 6.0.6.
所以理解了commit的定位以后,所有切换分支、切换tag、操作HEAD,本质上都是在对commit进行操作,这些操作的参数完全可以用commit id来替换HEAD、branch name、tag name等等。
总结
- svn的提交记录是一个按时间排序的线性结构,git的提交记录是一个参考时间的树状结构
- git记录中时间先后不能代表commit修改的先后,回溯查找时要注意这一点才能解释很多疑惑
- git中的commit我认为是它的精髓,通过commit的串联和别名,形成分支、标签、HEAD等多种元素,隐藏了细节,方便了操作
什么才是精彩的人生?扬在脸上的自信、长在心底的善良、融进血里的骨气、刻进生命里的坚强~