Git已经是我们日常工作开发中最常用到的分布式版本控制系统,大部分人也都知道如何使用它,知道有哪些命令。但同样很多人都在git操作中摔过跟头、翻过车,往往都是通过在网上搜索一番,按照搜索结果敲下命令,或许能解决或许不能。之所以这样,是因为你并不了解每个命令背后的原理,如果了解其本质,就可以更好的帮你梳理思路,而不会迷失在Git众多命令和参数上。这篇文章的目的是用“可视化”动画的形讲解日常开发中常用的命令,让你更好的了解Git的本质。
Git仓库数据结构
在开始之前,需要对Git有个大体的结构的认知–Git仓库的数据结构,如下图所示
- Git仓库由一个个的commit组成;
- 某个commit上会有一些branch指向它们,这些branch的本质是引用;
- 有一个特殊的引用叫做HEAD,它始终指向当前的位置,这个位置可以是commit,也可以是branch
在上面提到了三个概念:commit、branch、HEAD
commit:
- 表示对上一次改动的提交,它可以代表当前时刻下Git仓库的完整快照,但本质上,
commit只是记录了距离上一次commit之间的改动。 - git会根据提交的对象、作者和日期会生成一串很长哈希值(每个提交还包含上一个提交的hash值),这个哈希值是不能更改的,可以用它表示你的每一次提交。
branch:
- 分支,指的是长裤结构出现分叉时不同的叉,本质上git的
branch是引用(reference),指向某个commit的指针。 master是一个特殊的``branch,因为它是Git的默认branch(默认branch`可以修改)- 默认 branch 的特点:
- 执行 clone 方法把仓库取到本地的时候,默认 checkout 出来的是默认branch,即 master
- 在执行
push命令把本地内容推送到远端仓库的时候,远端仓库的HEAD永远跟随默认branch,而不是和本地HEAD同步。换句话说,只有push master分支到远端的时候,远端的HEAD才会移动
HEAD:
- 也是引用,但它不是
branch,它代表了当前所处的位置。HEAD不仅可以指向某个commit,也可以指向某个branch(例如 master、feature1) - 当每次
commit的时候,HEAD不仅随着新的commit一起移动,而且如果它指向了某个branch,那么它也会带着branch一起移动
Git通常用命令
了解完几个常用的概念后 ,我们开始探究一下开发中常用命令背后具体的操作:Commit、Checkout、Merge、Rebase、Reset、Cherry-Pick
Commit
当修改或增加完某个文件,我们先git add操作把内容提交到暂存区,再通过git commit命令将暂存区内容添加到本地仓库中。上面提到git会根据提交的内容生成一串hash值,而commit具体的过程是在HEAD指向的commit后面追加你新提交的commit。
git commit -m “提交信息”
Checkout
checkout branch
在需求开发的过程中,往往需要在不同的分支上来回切换,Checkout最常见的功能也就是切换分支,你只需运行如下命令:
git chekcout
Git会把HEAD引用切换到你需要的分支上,发生如下操作:
HEAD引用从原先master分支切换到feature1分支上,这样你就完成了分支切换的操作。
checkout commit
除了常见的分支切换,checkout还能切换指定的commit,同样也是HEAD引用的移动,只是HEAD指向是commit
git chekcout

这个命令是个只读操作,切换到某个commit后,它可以查看旧版本的文件,也可以修改任意的提交,但当你切回分支后,这时候你之前做的任何提交相当于放弃掉。如果你想保留之前创建的提交,可以在没切走之前创建一个新的分支来可以保留之前所创建的提交。
Merging
在开发中创建多个分支是必要的,以使每个新变更彼此分离,并确保你不会意外将未经批准或破损的变更推到生产分支上。但要将更改从一个分支转移到另一个分支上,有一种方法是执行git merge,Git可以执行两种类型的合并:快进(fast-forward)或无快进(no-fast-forward)
Fast-forward (--ff)
当前分支与我们要合并的分支相比没有额外的提交时,可能会发生快速合并。 Git首先会尝试执行最简单的选择:快进!此类合并不会创建新的提交,而是会在我们当前要合并的分支中合并这些提交。
我们可以在master分支上获得在feature1分支上所做的所有更改。那么什么是no-fast-forward的呢?
No-fast-foward (--no-ff)
如果当前的分支与你要合并的分支相比没有任何额外的提交,那就太好了,但是不幸的是,这种情况很少!如果需要合并的分支和你当前分支都做了改动,则git将执行no-fast-forward合并。使用无快进合并,Git在活动分支上创建一个新的合并提交,提交的父提交既指向当前分支又指向我们要合并的分支!
建议:每次合并的时候最好加上参数 –no-ff。因为主干分支往往都是比较稳定的代码,feature开发分支的许多提交都是零碎的,快进式合并代码会把feature开发分支的提交历史混入到主干分支上。
Rebasing
我们刚刚何通过执行git merge将更改从一个分支合入到另一个分支啥过。但是将更改从一个分支添加到另一个分支还有另一种方法是——执行git rebase。
git rebase从当前分支复制提交,并将这些复制的提交放在指定分支的顶部。
因为Git中的每一个commit都是不会改变的,所以rebase之后的每个commit其实都是新产生的,而不是对原先的commit进行修改,从上面的动画也能看出master原本的hash值发生了改变。这也就是rebase与merge合并相比最大的区别,使用rebase这样你就不会遇到任何合并冲突,并且可以保持良好的线性Git历史记录。
但个人的建议不要在较大且多人开发的项目中使用,因为git rebase会更改项目的历史记录,这会让回溯历史提交不好定位。
Resetting
reset把当前branch指向指定的commit,当你使用Git的时候可能commit提交代码后,发现这一次的commit的内容是错误的,这时候你可能需要用到git reset命令。想要更好的理解reset命令,需要先看看git的三大区工作区、暂存区、版本库
- 工作区 working directory此时文件处于 untracked(未追踪) 状态,此时使用git add命令可将文件加入到暂存区
- 暂存区 staging area此时文件处于 unstaged 状态,此时使用commit 命令将文件提交到版本库
- 版本库 repositoty
git reset命令常用的三种方式soft、hard和mixed.
soft reset
git reset –soft
移动HEAD到指定的commit节点,但保留 工作区和暂存区的内容,简单来说就是你的代码还在只是变成了未提交状态或未添加状态,如果想重新提交,需要commit即可
hard reset
git reset –hard
彻底丢掉当前版本的修改,并更改HAEAD移动到指定的commit节点;就是回退到指定的版本,不保留本地任何修改,慎用!
mixed reset
git reset –mixed
–mixed也是reset的默认参数可不加,它和soft reset很像,把HEAD移动到指定的commit上,回退版本库和暂存取的信息,不会回退工作区的信息,也就是如果你想再次提交,需要先add进暂存区,再执行commit命令。
Cherry-Picking
分支之间代码合并,最常见的情况是把一个分支所有的变动都合入到另一个分支上,你可以采用git merge或则git rebase。但是还有一种情况是,你只需要某个分支上部分代码变动(某几个提交),比如你在某个feature分支上开发,突然产品告诉你,你的这个需求不要了,但是其中某个能力是需要,这时可以采用``Cherry pick`的方式。
git cherry-pick c1h924

从上图中可以看到master分支上就引入了c1h924 commit的改动,只不过在master分支是一个新的commit提交。
Cherry pick 还支持一次转移多个提交。
git cherry-pick
上面的命令将 A 和 B 两个提交应用到当前分支。这会在当前分支生成两个对应的新提交。
如果想要转移一系列的连续提交,可以使用下面的简便语法。
git cherry-pick A..B v
上面的命令可以转移从 A 到 B 的所有提交。它们必须按照正确的顺序放置。
最后
Git作为目前世界上最先进的分布式版本控制系统最好的分布式系统没有之一,还有很多有趣的内容需要我们去学习,比如Git的三个分区、Git的存储形式、Git是如何做到历史记录不会被篡改等等,有兴趣的同学可以一起交流学习。
参考资料:
官方文档https://git-scm.com/book/en/v2