分支管理——git给开发人员最大的礼物。
关于“分支”这个词,我们似乎并不陌生,就是一套包含历史变更记录的代码的。但在svn时代,它表现的形式是 一个分支=一个目录。尽管管理员可以人为的给这些目录赋予上下级关系并且注明版本名,但他们在我们心里就是”一个一个并列的目录“。结果就是我们的工作空间里分了好多目录,如果管理不当,就会出现东一个西一个。开发完这个任务,要开发下一个任务的bug时就要用IDE换一个工作空间。
当分支越来越多,简直是灾难,无论是服务器上还是本地都混乱不堪。多年之后,可能还会出现分支找不到的问题。
用了git,你将感受到工作空间前所未有的”清爽“,工作空间永远”只有一套代码“,开发完一个任务,简单的切换一下分支,就可以去开发另外一个分支的任务,轻松实现“原地变身”。
找一个空目录作为自己的工作空间和本地版本库的位置,在该目录打开gitg命令行工具
1-1. git clone 远程版本库地址
1-2. git checkout -b b_new【等价于两个步骤:创建分支git branch b_new; 切换分支git chekcout b_new】
git add 文件名 或者 git add .(把所有更改的文件添加进暂存区,但一般不建议这么做,因为并不是所有文件我们都希望提交)
git stash save "暂存说明"
git checkout other_branch
6-1. 切换回来 git checkout b_new
6-2. 查看一下暂存列表 git stash list
能看到 stash@{0}: On master: 2020.11.24 b_new进度保存
6-3. git stash pop stash@{0}【根据注释 对照列表找到自己想要还原的内容】
git commit -m "注释说明”【注意:commit这一步不像svn那样可以部分提交,而是把所有暂存区的内容整体提交,因为在添加暂存区的时候就已经让我们做出了选择】
git push origin b_new
git add xxx;
git commit -m "xxx";
git push b_new;
10-1. 切换到master:git checkout master
10-2. 合并代码: git merge b_new【如果有冲突,需要手动处理】
10-3. 把master推送到远程:git push origin master
这一步我一般习惯使用命令去完成,然后在idea里导入代码,idea会自动识别并关联git。
或
使用右上角的快捷按钮
弹出确认框,这里可以双击文件确认推送的内容
–>-->–>-->–>-如果你是刚使用git的新手,至此,你的任务已经完成,后面的事情不需要你去做了。因为涉及到合并master,如果你不清楚明白的知道在做什么,最好由其他有经验的人去做–>-->–>-->–>-->–>--
合并完成后,把master合并完的内容推送到远程。注意:这个合并是直接合到本地master版本库的,所以在ChangeList是没有东西的,下一步只剩下push
git这么好的东西,底层是如何实现的呢?
这是最简单粗暴的想法,占空间是一定的,当然简单有简单的好处,算法结构简单也会让软件更不容易出问题。我们知道git号称每次提交都对整个项目树产生一个唯一的hash1编码,莫非git真的使用的这么简单粗暴的存储方式?
比如文件a,第一次是234,第二次提交的是增加了56 变成23456,那么就记录“+ 56",这样存储一定是最省空间的,但同时造成的软件复杂度,可能会让git变得不可用:比如我存储的是个图片,中间对该图片进行了美化(修改的是一些二进制数据)。
兼具节省空间和"全量保存”:
git的存储内容分为两部分:实体文件库 + 索引树
实体文件库就是存真实的文件,比如 hello.java , cc.jpg。
索引树是一颗树形结构的数据结构,叶子节点上不是文件,而是文件的索引,该索引指的位置就是 实体问价库的位置。说到这里,你是否恍然大悟,下面简单示意一下大致原理:
实体文件库里就存了一个文件(1代表文件的索引)
索引 | 文件 |
---|---|
1 | README.txt |
索引树的结构就是(tree1是索引树的版本号)
------tree1------
demo
实体文件库就会增加一个文件
索引 | 文件 |
---|---|
1 | README.txt |
2 | hello.java |
索引树的结构(注意tree1还在,现在是增加了一棵树tree2,这样我们才有回退版本的可能)
------tree1------
------tree2------
此时实体文件库就会增加一个文件(实际上git在存储文件库的时候并不会使用文件原始名字存储,这里只是为了让读者能清晰的看到实际存储的文件)
索引 | 文件 |
---|---|
1 | README.txt |
2 | hello.java |
3 | hello.java |
索引树的结构(注意tree1,tree2还在,现在是增加了一棵树tree3)
------tree1------
------tree2------
------tree3------
当然,实际的存储结构会做很多优化,从而比示意的要复杂的多,最终得以让git实现版本分支管理的前提下,达到占用空间最小,存取速度最快的效果。
有了上面的存储结构的铺垫,再去理解git核心概念原理就容易些,看下面这张常见的基本原理图。
Git存储使用的是一个内容寻址的文件系统,其核心部分是一个简单的键值对(key-value)数据库,当向数据库中插入任意类型的内容,它会返回一个40位十六进制的SHA-1哈希值用作索引。
在版本库中,Git维护的数据结构有:以下4种对象及索引,并通过保存commitID有向无环图的log日志来维护与管理项目的修订版本和历史信息。
下面是git的三种重要的数据类型的关系图
每次提交,都会产生一个commit,commit中记录了上一个节点的commit ID,作者,指向一个版本的目录tree对象。
可以使用命令 git cat-file -p xxxx
表示1个目录,记录着目录里所有文件blob哈希值、文件名子目录名及其他元数据。通过递归引用其他目录树,从而建立一个包含文件和子目录的完整层次结构。我们每次提交记录,就会产生一个新的版本,对应的关键信息就是这棵树,由于只是一颗tree信息,虽然看起来像是“复制”了整个项目,但占用空间并不大,通过这棵树,git得以给每个版本的整个项目生成一个唯一的hash值,从而保证历史不被篡改。
保存某个文件的某个版本的数据。一个文件只要有改动,就会产生一个新的blob对象存储该文件。而不改动的文件,会一直被每个版本的tree引用,从而节省了大量存储文件的空间。注意:只要做了add操作,文件就会被存储进来,所以某些体积很大,但肯定不需要版本控制的(最典型的就是编译完的包),一定添加到.gitignore里,否则你一个不小心,git仓库就变大好多(当然,git有像java一样的gc操作,可以定期清理掉这种没有人引用的垃圾数据)
分为轻量标签和附注标签。轻量标签实际上是一个特定提交的引用,附注标签是存储在git中的一个完整可被校验的对象(保存在.git/refs/tags中),还包含打标签者的名字、e-mail、日志、注释等信息
git是大名鼎鼎的Linux之父的又一杰作,Linux的一个特点就是“一切皆文件”,git当然也会延续Linux的特点,git给我们带来方便的同时必然也带来了很多新的概念,这些概念并不是空洞的的理论,而是能看得见摸得着的文件,下面我去对应一下加深一下理解
这是项目jdemo根目录下的文件目录,其中有两个是git添加的
.git : git本地库目录,是个隐藏目录,也是git的核心目录
.gitignore: 这个并不是必需的,是个隐藏文件,用于在提交时忽略一些文件,即使这些文件变更了,也会被忽略提交
下面主要看.git的内容
里面内容很多,先看圈中的几个涉及重要概念的文件及目录
但不要认为这里存的只有文件本身,实际上这里面包含了git的所有存储数据,包括索引树。就像数据库的数据库文件,里面包含数据库结构和一条条的数据记录。
如果你打开这个目录,会看到时这个样子
这是里面的文件
由于都是二进制文件,无法直接解读里面的内容,但可以使用工具命令
git cat-file -t xxxx来查看对象的数据类型(commit/tree/blob/tag)
git cat-file -p xxxx来查看对象的属性信息
git show xxxx来查看对象的内容
刚才看了git实际存储数据的文件,可以发现存储结构很复杂,而且文件名也不容易记,而我们在使用的过程中面对的分支名并没有这么复杂,一般都是 master、分支1,分支2,,,,什么的,那么这些名字和实际的数据是怎么对应的呢,就在refs目录,这个目录里的数据就很简单了,而且是明文字符串
这三个目录分别对应我们本地的分支、远程分支、里程碑
。以本地分支为例
其中b_new就是我们创建的分支名字,文件里的内容就是该分支的“索引”
可以看到这是一串字符串,这个字符串就指向了某个提交节点。有个比较特殊的一个引用是HEAD,
就在.git根目录(.git\refs\remotes\origin里也有一个)
里面的内容是
ref: refs/heads/master
它指向另外一个引用(一般指向的就是master引用的位置),所以我们说:
Git 分支的本质:一个指向某一系列提交之首的指针或引用
我们还可以通过这个字符串(引用)找到对应的存储文件,比如把上面b_new文件里的那个字符串引用拆成两部分,在objects里找
目录名为27,文件名为096de7b0614778557c12de57feaa896c09b612的文件,如图所示
使用命令看一下这是个什么类型的文件:git cat-file -t 096de7b0614778557c12de57feaa896c09b612
结果报错:fatal: Not a valid object name 096de7b0614778557c12de57feaa896c09b612
因为096de7b0614778557c12de57feaa896c09b612并不是对象的hash ID,要把目录名拼在一起,才是一个完整的40位hash值
27096de7b0614778557c12de57feaa896c09b612
可以看到该文件是一个commit类型的文件(从文件大小只有1k也能大致猜出来)
然后使用命令git cat-file -p 27096de7b0614778557c12de57feaa896c09b612看一下文件属性信息
再使用命令git show 27096de7b0614778557c12de57feaa896c09b612查看文件内容
当你修改的代码还没有commit的时候一定不要先使用idea的"update"功能更新代码(希望先处理一下冲突再提交)。两个原因:
当修改的代码还没有commit的时候,如果不清楚自己在做什么(对git特性理解不够透彻),不要切换分支。在切换之前要先用stash暂存该分支的工作进度,让ChangeList处于清空状态再切换分支。否则一个不留神,就把本该在这个分支提交的代码,提交的其他分支上去了。
我们知道git最终会产生一条版本树节点链条,结合我们开发的感觉,你可能会产生这样的想法:链条的前后节点是有“前后因果的延续”,比如节点1是 abc, 节点2是abcde, 是因为节点2在节点1的基础上增加de的结果。
但当我们合并的时候, 如果我们手动合并后的代码变化极大,另一个分支把当前我们的分支的特性全部抹除了,git是否会“感到困惑”,比如 我们的分支是abc, 合并完变成了123。git能接受这种合并结果吗? 其实这就是你想多了,要明白,git再智能,它也只是一个版本管理工具,它并不理解我们的代码内容,它其实也不关心 版本1 和版本2之间到底经历了什么蜿蜒曲折的变更过程。它只关心版本2是否能通过合并代码确定下来(它如果确定不了,就会让你手动合并),至于确定下来是什么样的结果 和上一版本差异由多大,它并不关心(版本差异大,对git来说无非就是存储的内容多了一点),通过前面关于git的存储结构的介绍,也能解答这个疑惑:git的存储是以文件为单位的。
一次提交一整块可用内容。也就是不要改两行代码,因为临时有其他事情,或者要下班了 就草草提交。这样做 就会让“版本分支管理” 退化成 “分支管理”,这个时候我们应该利用好前面说的stash暂存功能。
公司资料分为两种:代码,文档
这两种资料的管理目标并不一样,代码除了有中央共享协作的功能,一个很重要的目标就是清晰的版本和分支管理。
而文档资料对版本管理的要求并没有代码那么高,因为大家很少看一个word文档以往的提交变更记录。即使要控制版本,往往也是直接在文档的开头添加一个“版本变更说明表”。也就是我们更看重文档的结果,而不是中间“加一个逗号,去一个句号”这些繁琐而且很难“分块”的内容。文档资料虽然对历史版本的管理不像代码有那么高的要求,但也有自己独特的需求点:结构清晰 所见即所得 同时可以部分检出。
应对这两种需求 我觉的目前最好的工具:代码管理使用git,文档管理使用微软的TFS
git强大的版本分支管理能力上文已经讲述。现在讲一下TFS在文档管理的优势:
Git权威指南
官方中文文档
git在线模拟游戏
IDEA中Git的使用
Git原理与命令大全