图解git原理与平常实用指南

缘起

读了“扔物线”老师的小册《Git 原理详解及实用指南》感受收获良多,因而想写点东西作一个总结,即加深本身的印象也但愿能给社区小伙伴一点帮助,写的不对的地方还请多多指导。身为一个初入前端半年的菜鸟,由伊始的只知道git是用来托管代码的工具到逐步了解中央版本控制系统与分布式版本控制系统(git)的原理与区别;从以前只会基本的add、commit、pull、push操做到使用stash、merge、reset方便得不亦乐乎,都得益于对git原理的深刻理解,逼话少说,咋们直接进入正题。前方长篇预警...前端

从了解版本控制系统开始

所谓版本控制,就是在文件修改的历程中保留修改历史,能够方便的撤销(如同文本编辑的撤销操做通常,只是版本控制会复杂的多)以前对文件的修改。一个版本控制系统的三个核心内容:版本控制(最基本的功能),主动提交(commit历史)和远程仓库(协同开发)。git

中央式版本控制系统(VCS)

工做模型安全

  1. 主工程师搭好项目框架
  2. 在公司服务器建立一个远程仓库,并提交代码
  3. 其余人拉取代码,并行开发
  4. 每一个人独立负责一个功能,开发完成提交代码
  5. 其余人随时拉取代码,保持同步

分布式版本控制系统(DVCS)

分布式与中央式的区别主要在于,分布式除了远程仓库以外团队中每个成员的机器上都有一份本地仓库,每一个人在本身的机器上就能够进行提交代码,查看版本,切换分支等操做而不须要彻底依赖网络环境。
工做模型服务器

  1. 主工程师搭好项目框架 ,并提交代码到本地仓库
  2. 在公司服务器建立一个远程仓库,并将1的提交推送到远程仓库
  3. 其余人把远程仓库全部内容克隆到本地,拥有了各自的本地仓库,开始并行开发
  4. 每一个人独立负责一个功能,能够把每个小改动提交到本地(因为本地提交无需当即上传到远程仓库,因此每一步提交没必要是一个完整功能,而能够是功能中的一个步骤或块)
  5. 功能开发完毕,将和这个功能相关的全部提交从本地推送到远程仓库
  6. 每次当有人把新的提交推送到远程仓库的时候,其余人就能够选择把这些提交同步到本身的机器上,并把它们和本身的本地代码合并

分布式版本管理系统的优缺点:

优势

  • 大多数操做本地进行,数度更快,不受网络与物理位置限制,不联网也能够提交代码、查看历史、切换分支等等
  • 分布提交代码,提交更细利于review

缺点

  • 初次clone时间较长
  • 本地占用存储高于中央式系统

继续深刻git原理

假设你已经安装好了git并将代码clone到了本地,新手移步git安装与代码拷贝指南markdown

git最基本的工做模型

首先理解三个基本概念:
网络

  • 工做区:就是你在电脑里能看到的目录
  • 版本库:工做区有一个隐藏目录.git,这个不算工做区,而是Git的本地版本库,你的全部版本信息都会存在这里
  • 暂存区:英文叫stage, 或index。通常存放在 ".git目录下" 下的index文件(.git/index)中,因此咱们把暂存区有时也叫做索引(index)

工做模型
1.首先新建一个test.txt文件并对其进行修改,经过status能够查看工做目录当前状态,此时test.txt对git来讲是不存在的(Untracked)

2.而后经过add命令将修改放入暂存区(git开始追踪它)

能够看到,test.txt 的文字变成了绿色,它的前面多了「new file:」的标记,而它的描述也从 "Untracked files" 变成了 "Changes to be commited"。这些都说明一点:test.txt 这个文件的状态从 "untracked"(未跟踪)变成了 "staged"(已暂存),意思是这个文件中被改动的部分(也就是这整个文件)被记录进了 staging area(暂存区)

stage 这个词在 Git 里,是「集中收集改动以待提交」的意思;而 staging area ,就是一个「聚集待提交的文件改动的地方」。简称「暂存」和「暂存区」。至于 staged 表示「已暂存」,就不用再解释了吧?
3.如今文件已经放入暂存区,能够用commit命令提交:

在这里你也能够直接commit提交会进入commit信息编辑页面,而加上-m参数能够快捷输入简短的提交备注信息,这样你就完成了一次提交(能够经过 git log查看提交历史)

接着对该文件再次进行修改,输入 git status能够看到,该文件 又变红了,不过此次它左边的文字不是 "New file:" 而是 "modified:",并且上方显示它的状态也不是 "Untracked" 而是 "not staged for commit",意思很明确:Git 已经认识这个文件了,它不是个新文件,但它有了一些改动。因此虽然状态的显示有点不一样,但处理方式仍是同样的:
接下来再次将该文件add、commit,查看log能够看到已经存在两条提交记录

4.最后经过push把本地的全部commit上传到远程仓库:

团队工做基本模型

工做模型
1.在上面基本操做的基础上,同事 commit 代码到他的本地,并 push 到远程仓库
2.你把远程仓库新的提交经过 pull指令拉取到你的本地

经过这个流程,你和同事就能够简单地合做了:你写了代码,commit,push 到远程仓库,而后他 pull 到他的本地;他再写代码,commit, push 到远程仓库,而后你再 pull 到你的本地。你来我往,配合得不亦乐乎。(可是有时候push会失败)app

为何会失败?
由于 Git 的push 实际上是用本地仓库的commit记录去覆盖远程仓库的commit记录(注:这是简化概念后的说法,push 的实质和这个说法略有不一样),而若是在远程仓库含有本地没有的commit的时候,push (若是成功)将会致使远端的commit被擦掉。这种结果固然是不可行的,所以 Git 会在 push 的时候进行检查,若是出现这样的状况,push 就会失败框架

这时只须要先经过git pull(实为fetch和merge的组合操做)将本地仓库的提交和远程仓库的提交进行合并,而后再push就能够了分布式

Feature Branching:最流行的工做流

核心:
(1)任何新的功能(feature)或 bug 修复全都新建一个 branch 来写;
(2)branch 写完后,合并到 master,而后删掉这个 branch(可以使用git origin -d 分支名删除远程仓库的分支)。 工具

优点:
(1)代码分享:写完以后能够在开发分支review以后再merge到master分支
(2)一人多任务:当正在开发接到更重要的新任务时,你只要稍微把目前未提交的代码简单收尾一下,而后作一个带有「未完成」标记的提交(例如,在提交信息里标上「TODO」),而后回到 master 去建立一个新的 branch 进行开发就行了。

HEAD、branch、引用的本质以及push的本质

HEAD:当前commit的引用

当前 commit 在哪里,HEAD 就在哪里,这是一个永远自动指向当前 commit 的引用,因此你永远能够用 HEAD 来操做当前 commit,

branch:

HEAD 是 Git 中一个独特的引用,它是惟一的。而除了 HEAD 以外,Git 还有一种引用,叫作 branch(分支)。HEAD 除了能够指向 commit,还能够指向一个branch,当指向一个branch时,HEAD会经过branch间接指向当前commit,HEAD移动会带着branch一块儿移动:

branch 包含了从初始 commit 到它的全部路径,而不是一条路径。而且,这些路径之间也是彼此平等的。

像上图这样,master 在合并了 branch1 以后,从初始 commit 到 master 有了两条路径。这时,master 的串就包含了 1 2 3 4 7 和 1 2 5 6 7 这两条路径。并且,这两条路径是平等的,1 2 3 4 7 这条路径并不会由于它是「原生路径」而拥有任何的特别之处

建立branch: git branch 名称
切换branch: git checkout 名称(将HEAD指向该branch)
建立+切换: git checkout -b 名称
在切换到新的 branch 后,再次 commit 时 HEAD 就会带着新的 branch 移动了:
而这个时候,若是你再切换到 master 去 commit,就会真正地出现分叉了:
删除branch: git branch -d 名称
注意:
(1)HEAD 指向的 branch 不能删除。若是要删除 HEAD 指向的 branch,须要先用 checkout 把 HEAD 指向其余地方。
(2)因为 Git 中的 branch 只是一个引用,因此删除 branch 的操做也只会删掉这个引用,并不会删除任何的 commit。(不过若是一个 commit 不在任何一个 branch 的「路径」上,或者换句话说,若是没有任何一个 branch 能够回溯到这条 commit(也许能够称为野生 commit?),那么在必定时间后,它会被 Git 的回收机制删除掉)
(3)出于安全考虑,没有被合并到 master 过的 branch 在删除时会失败(怕误删未完成branch)把-d换成-D能够强制删除

引用的本质

所谓引用,其实就是一个个的字符串。这个字符串能够是一个 commit 的 SHA-1 码(例:c08de9a4d8771144cd23986f9f76c4ed729e69b0),也能够是一个 branch(例:ref: refs/heads/feature3)。

Git 中的 HEAD 和每个 branch 以及其余的引用,都是以文本文件的形式存储在本地仓库 .git 目录中,而 Git 在工做的时候,就是经过这些文本文件的内容来判断这些所谓的「引用」是指向谁的。

push的本质:把 branch 上传到远程仓库

(1)把当前branch位置上传到远程仓库,并把它路径上的commits一并上传
(2)git中(2.0及之后版本),git push不加参数只能上传到从远程仓库clone或者pull下来的分支,如需push在本地建立的分支则需使用git push origin 分支名的命令
(3)远端仓库的HEAD并不随push与本地一致,远端仓库HEAD永远指向默认分支(master),并随之移动(可使用git br -r查看远程分支的HEAD指向)。

开启git操做之旅

merge:合并

含义:从目标 commit 和当前 commit (即 HEAD 所指向的 commit)分叉的位置起,把目标 commit 的路径上的全部 commit 的内容一并应用到当前 commit,而后自动生成一个新的 commit。

当执行 git merge branch1操做,Git 会把 5 和 6 这两个 commit 的内容一并应用到 4 上,而后生成一个新的提交 7 。
merge的特殊状况:
(1)merge冲突:你的两个分支改了相同的内容,Git 不知道应该以哪一个为准。若是在 merge 的时候发生了这种状况,Git 就会把问题交给你来决定。具体地,它会告诉你 merge 失败,以及失败的缘由;这时候你只须要手动解决掉冲突并从新add、commit(改动不一样文件或同一文件的不一样行都不会产生冲突);或者使用 git merge --abort放弃解决冲突,取消merge
(2)HEAD 领先于目标 commit:merge是一个空操做:
此时merge不会有任何反应。
(3)HEAD 落后于 目标 commit且不存在分支(fast-forward):
git会直接把HEAD与其指向的branch(若是有的话)一块儿移动到目标commit。

rebase:给commit序列从新设置基础点

有些人不喜欢 merge,由于在 merge 以后,commit 历史就会出现分叉,这种分叉再汇合的结构会让有些人以为混乱而难以管理。若是你不但愿 commit 历史出现分叉,能够用 rebase 来代替 merge。

能够看出,经过 rebase,5 和 6 两条 commits 把基础点从 2 换成了 4 。经过这样的方式,就让原本分叉了的提交历史从新回到了一条线。这种「从新设置基础点」的操做,就是 rebase 的含义。另外,在 rebase 以后,记得切回 master 再 merge 一下,把 master 移到最新的 commit。

为何要从 branch1 来 rebase,而后再切回 master 再 merge 一下这么麻烦,而不是直接在 master 上执行 rebase?

从图中能够看出,rebase 后的每一个 commit 虽然内容和 rebase 以前相同,但它们已是不一样的 commit 了(每一个commit有惟一标志)。若是直接从 master 执行 rebase 的话,就会是下面这样:

这就致使 master 上以前的两个最新 commit (3和4)被剔除了。若是这两个 commit 以前已经在远程仓库存在,这就会致使无法 push :
因此,为了不和远程仓库发生冲突,通常不要从 master 向其余 branch 执行 rebase 操做。而若是是 master 之外的 branch 之间的 rebase(好比 branch1 和 branch2 之间),就没必要这么多费一步,直接 rebase 就好。

须要说明的是,rebase 是站在须要被 rebase 的 commit 上进行操做,这点和 merge 是不一样的。

stash:临时存放工做目录的改动

stash 指令能够帮你把工做目录的内容所有放在你本地的一个独立的地方,它不会被提交,也不会被删除,你把东西放起来以后就能够去作你的临时工做了,作完之后再来取走,就能够继续以前手头的事了。
操做步骤:
(1)git stash能够加上save参数后面带备注信息(git stash save '备注信息'
(2)此时工做目录已经清空,能够切换到其余分支干其余事情了
(3)git stash pop弹出第一个stash(该stash从历史stash中移除);或者使用git stash apply达到相同的效果(该stash仍存在stash list中),同时可使用git stash list查看stash历史记录并在apply后面加上指定的stash返回到该stash。
注意:没有被track的文件会被git忽略而不被stash,若是想一块儿stash,加上-u参数。

reflog:引用记录的log

能够查看git的引用记录,不指定参数,默认显示HEAD的引用记录;若是不当心把分支删掉了,可使用该命令查看引用记录,而后使用checkout切到该记录处重建分支便可。

注意:再也不被引用直接或间接指向的 commits 会在必定时间后被 Git 回收,因此使用 reflog 来找回被删除的 branch 的操做必定要及时,否则有可能会因为 commit 被回收而再也找不回来。

看看我都改了什么

log:查看已提交内容

git log -p能够查看每一个commit的改动细节(到改动文件的每一行)
git log --stat查看简要统计(哪几个文件改动了)
git show 指定commit 指定文件名查看指定commit的指定文件改动细节

diff:查看未提交内容

git diff --staged能够显示暂存区和上一条提交之间的不一样。换句话说,这条指令可让你看到「若是你当即输入 git commit,你将会提交什么」
git diff能够显示工做目录和暂存区之间的不一样。换句话说,这条指令可让你看到「若是你如今把全部文件都 add,你会向暂存区中增长哪些内容」
git diff HEAD能够显示工做目录和上一条提交之间的不一样,它是上面这两者的内容相加。换句话说,这条指令可让你看到「若是你如今把全部文件都 add 而后 git commit,你将会提交什么」(不过须要注意,没有被 Git 记录在案的文件(即历来没有被 add 过的文件,untracked files 并不会显示出来。由于对 Git 来讲它并不存在)实质上,若是你把 HEAD 换成别的commit,也能够显示当前工做目录和这条 commit 的区别。

刚刚提交的代码发现写错了怎么办?

再提一个修复了错误的commit?能够是能够,不过还有一个更加优雅和简单的解决方法:commit --amend。
具体作法:
(1)修改好问题
(2)将修改add到暂存区
(3)使用git commit --amend提交修改,结果以下图:

减小了一次无谓的commit。

错误不是最新的提交而是倒数第二个?

使用rebase -i(交互式rebase):
所谓「交互式 rebase」,就是在 rebase 的操做执行以前,你能够指定要 rebase 的 commit 链中的每个 commit 是否须要进一步修改,那么你就能够利用这个特色,进行一次「原地 rebase」。

操做过程:
(1)git rebase -i HEAD^^

说明:在 Git 中,有两个「偏移符号」: ^ 和 ~。
^ 的用法:在 commit 的后面加一个或多个 ^ 号,能够把 commit 往回偏移,偏移的数量是 ^ 的数量。例如:master^ 表示 master 指向的 commit 以前的那个 commit; HEAD^^ 表示 HEAD 所指向的 commit 往前数两个 commit。
~ 的用法:在 commit 的后面加上 ~ 号和一个数,能够把 commit 往回偏移,偏移的数量是 ~ 号后面的数。例如:HEAD~5 表示 HEAD 指向的 commit往前数 5 个 commit。

上面这行代码表示,把当前 commit ( HEAD 所指向的 commit) rebase 到 HEAD 以前 2 个的 commit 上:

(2)进入编辑页面,选择commit对应的操做,commit为正序排列,旧的在上,新的在下,前面黄色的为如何操做该commit,默认pick(直接应用该commit不作任何改变),修改第一个commit为edit(应用这个 commit,而后停下来等待继续修正)而后:wq退出编辑页面,此时rebase停在第二个commit的位置,此时能够对内容进行修改:
(3)修改完后使用add,commit --amend将修改提交
(4) git rebase --continue继续 rebase 过程,把后面的 commit 直接应用上去,此次交互式 rebase 的过程就完美结束了,你的那个倒数第二个写错的 commit 就也被修正了:

想直接丢弃某次提交?

reset --hard 丢弃最新的提交

git reset --hard HEAD^

HEAD^ 表示 HEAD 往回数一个位置的 commit ,上节刚说过,记得吧?

用交互式 rebase 撤销历史提交

操做步骤与修改历史提交相似,第二步把须要撤销的commit修改成drop,其余步骤再也不赘述。

用 rebase --onto 撤销提交

git rebase --onto HEAD^^ HEAD^ branch1
上面这行代码的意思是:以倒数第二个 commit 为起点(起点不包含在 rebase 序列里),branch1 为终点,rebase 到倒数第三个 commit 上。

错误代码已经push?

有的时候,代码 push 到了远程仓库,才发现有个 commit 写错了。这种问题的处理分两种状况:

出错内容在本身的分支

假如是某个你本身独立开发的 branch 出错了,不会影响到其余人,那不要紧用前面几节讲的方法把写错的 commit 修改或者删除掉,而后再 push 上去就行了。可是此时会push报错,由于远程仓库包含本地没有的 commits(在本地已经被替换或被删除了),此时直接使用git push origin 分支名 -f强制push。

问题内容已合并到master

(1)增长新提交覆盖以前内容
(2)使用git revert 指定commit 它的用法很简单,你但愿撤销哪一个 commit,就把它填在后面。如:git revert HEAD^
上面这行代码就会增长一条新的 commit,它的内容和倒数第二个 commit 是相反的,从而和倒数第二个 commit 相互抵消,达到撤销的效果。在 revert 完成以后,把新的 commit 再 push 上去,这个 commit 的内容就被撤销了。它和前面所介绍的撤销方式相比,最主要的区别是,此次改动只是被「反转」了,并无在历史中消失掉,你的历史中会存在两条 commit :一个原始 commit ,一个对它的反转 commit。

reset:不止能够撤销提交

git reset --hard 指定commit你的工做目录里的内容会被彻底重置为和指定commit位置相同的内容。换句话说,就是你的未提交的修改会被所有擦掉。
git reset --soft 指定commit会在重置 HEAD 和 branch 时,保留工做目录和暂存区中的内容,并把重置 HEAD 所带来的新的差别放进暂存区。
什么是「重置 HEAD 所带来的新的差别」?就是这里:

git reset --mixed(或者不加参数) 指定commit保留工做目录,而且清空暂存区。也就是说,工做目录的修改、暂存区的内容以及由 reset 所致使的新的文件差别,都会被放进工做区。简而言之,就是「把全部差别都混合(mixed)放在工做区中」。

checkout:签出指定commit

checkout的本质是签出指定的commit,不止能够切换branch还能够指定commit做为参数,把HEAD移动到指定的commit上;与reset的区别在于只移动HEAD不改变绑定的branch;git checkout --detach能够把 HEAD 和 branch 脱离,直接指向当前 commit。

最后

但愿个人总结能给你们带来些许帮助,也但愿和你们一块儿学以至用,一块儿成长。最后,万分感谢扔老师的小册,强势安利《git原理详解与实用指南》,认准扔物线。

相关文章
相关标签/搜索