Git基本技能 – 温故知新系列
日常开发中用的最多的是git add、git commit、git pull、git fetch、git push等,不过当出现一些稍复杂一点的场景,如果具备相应的git知识储备,就很有可能脱颖而出。本文首先阐述git的一些基本原理,然后对开发中比较常遇到的合并和撤销等场景做讲解,避免死记硬背。
前言
版本控制工具采用的存储方式:
- 存储差异:存储base文件,之后的版本存储base文件的更改,SVN
- 存储快照:每次更改都存储一个新文件,Git(类似于immutable的原理?)
内部原理
先来认识下 .git 目录下几个比较重要的文件:
config:仓库的配置文件 HEAD:当前所在的位置,指向具体分支 refs/:存储的是引用文件,如本地分支,远端分支,标签等(其内容为40位hash,指向commit节点) objects/:数据文件的存储目录 hooks/: 钩子目录,在特定的重要动作发生时触发自定义脚本
这里重点介绍下objects,仓库中每个文件都有对应的hash值,包括3种数据类型:文件( blob
)、文件夹( tree
)、提交( commit
),其内容分别如下:
1. blob文件:具体文本 2. tree文件: 100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb 3. commit文件: tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 author Scott Chacon 1243040974 -0700 committer Scott Chacon 1243040974 -0700 first commit
可以看到,git对文件的管理是通过记录的文件hash,然后通过hash查找对应文件内容的方式。
最后看一张git仓库的原理图:
[注]HEAD:指向当前所在的分支;ref/head/ :指向当前的commit版本;ci :commit节点,包含当前版本的文件引用
理解commit节点间的引用关系,对理解分支的新建、切换、合并、撤销等至关重要。
由于每个分支 ref
指向的是 commit
节点,而commit通过 parent
指针指向前一次提交节点,实现链式提交记录的回溯
总结:git操作时,关键还是指针的移动,这也是为什么git又名 – 内容寻址系统
理解了上面的基本知识,下面对合并和撤销操作做讲解就会简单很多。
合并
部分提交
//合并某个提交 git cherry-pick //合并多个提交 git cherry-pick //合并连续提交(不含A) git cherry-pick A..B //合并连续提交(含A) git cherry-pick A^..B
代码冲突时:
1. 解决冲突 //解决冲突后git add . git cherry-pick --continue 2. 退出合并 git cherry-pick --abort
全部提交
merge
git merge [待合并的分支名] # 3种模式 -fast-forward:默认方式,如果待合并的分支在当前分支的下游,即也就是说没有分叉时,会发生快速合并 –no-ff:在当前分支上新建一个提交节点,从而完成合并 –squash:和no-ff非常类似,区别只有一点不会保留对合入分支的引用
代码冲突时:
1. 解决冲突 //解决冲突后git add . git commit -m 'xx' 2. 退出合并 git merge --abort
rebase
git rebase [作为合并基准的分支名] //一般使用:比如feature要合并到master 1)先在feature分支rebase master,即将feature的提交应用到master节点后 2)再checkout到master使用merge移动master指针到最新节点(fast-forward)
代码冲突时:
1. 解决冲突 //解决冲突后git add . git rebase --continue 2. 退出合并 git rebase --abort
两者的区别
假定当前分支情况如下
{master} | A--B--C | D--E | {feature}
1)合并待合并分支的提交信息后,生成一个新的提交节点
2)保留分支的提交合并记录,便于掌握历史操作记录
//在master分支使用merge feature> git merge master {master} | A--B--C--F | / D--E | {feature}
1) 指定目标分支作为基准点,将当前分支的节点依次patch到目标分支最新提交后
注意这里仅改变了待合并分支的指针位置,需要checkout到目标分支使用merge,则会直接将目标分支指针移动到最新提交节点
2) 不会删除或复用待合并的提交,而是依次创建新的提交应用到目标分支
3) 不会保留合并记录,使得提交记录线性化
//在feature分支使用rebase feature> git rebase master {master} {feature} | | A--B--C--D'--E' | D--E
至于在开发中是何时使用merge,何时使用rebase,这个见仁见智。(个人觉得主分支保持清爽很重要,但是特殊情况下撤销merge的分支合并更方便)
撤销
工作区内容撤销
//使用暂存区内容覆盖工作区 git checkout -- fileName
暂存区内容撤销
reset
主要用途:回退版本内容
一般格式
git reset
常用 option
--hard: //使用指定的提交版本覆盖工作区和暂存区。即删除指定版本后的所有提交内容 --soft://将指定提交之后的内容回退到暂存区,工作区不变动 --mixed: //将指定提交之后的commit以及暂存区的内容退回到工作区未保存状态
对于本地仓库,都是指针的变更;文件或要舍弃( hard
),或要合并提交( soft
),或要修改( mixed
)
举例git reset命令既可以回退版本,也可以把暂存区的修改回退到工作区
//把所有暂存区中的修改回退到工作区未保存状态git reset HEAD //默认为--mixedgit rest HEAD file
回退版本的指定
HEAD^: 上一个版本HEAD^^:上上一个版本...HEAD~n: 比如回退到倒数第3个版本,HEAD~3
回退版本后,想要提交到远程仓库,则需要使用 -f
强推:
push -f -u origin master
如果回退版本后,想要回到撤销前的版本,则可以使用git reflog查看对应的commitId。git reflog会列出所有历史提交记录,和git log的差异在于包含删除的提交记录
git reset --hard HEAD@{N}
revert
主要用途:撤销某个提交操作
1) 撤销某个普通commit
对指定的commit进行撤销操作,生成一个新的提交记录
git revert
2) 撤销某个merge commit
先来看合并两个分支后生成的commit节点信息
git show cs38864 commit bd868465569400a6b9408050643e5949e8f2b8f5 //有两个parant commit,指明当前commit节点由哪两个commit合并 Merge: ba25a9d 1c7036f
执行撤销时,需要指定主线分支,将会撤销另一分支内容
/** *-m表示是一个merge节点 *parent-number:1 | 2 表示保留哪个parent节点,顺序为上例中Merge中的commit **/ git revert -m parent-number
对于撤销的合并分支上的提交,后续再合并,撤销的节点也不会被合并;想要将撤销的分支提交重新合并,需对revert生成的commit进行一次撤销。
两者的区别
1.revert:不会删除提交记录,保留每一步操作记录,更安全 reset:不包含已删除的合并提交 2.revert因为是对指定提交进行取反操作,创建一个新的提交,因此无需强推到远程仓库 3.建议:本地reset,公共分支revert 4.合并后如果做了操作,reset到合并节点前,会删除之后的提交信息;可以使用reset达到revert的效果,如下: //回退提交内容到工作区 git reset devb-3 //将工作区的内容暂存stash git stash //使用hard改变(删除)指针commit git reset --hard devb-2 //恢复stash的内容到工作区 git stash pop //提交 git add & commit & push
拾遗
1.对上一次提交进行修改(打补丁),会创建一个新的commitId
git commit --amend //修改提交信息 git commit --amend -m 'xxx' //代码逻辑的修补 git commit --amend –-no-edit
2.合并多个提交记录
方式1
git reset --soft commitId //将制定commit后的提交信息回退到暂存区 git commit -m '合并多个commit'
方式2
# (start-commit, end-commit] 前开后闭区间,默认 end-commit 为当前 HEAD git rebase -i [start-commit] [end-commit]
3.查看不同阶段代码的差异
git diff: 查看工作区和暂存区的差异 git diff --cached HEAD:暂存区和当前仓库指针的差异
4.git stash
将工作区和暂存区的内容存储起来;默认状态,git stash命令会将工作区和暂存区内容重置为最近一次提交后的内容,并且只能将已经跟踪和非.gitignore忽略的文件储藏,未跟踪的文件不会被存储。如果想要将未跟踪的文件一并存储,使用 -u
( --include-untracked
)
git stash push -u
The latest stash you created is stored in refs/stash
; older stashes are found in the reflog of this reference and can be named using the usual reflog syntax (e.g. stash@{0}
is the most recently created stash, stash@{1}
is the one before it, stash@{2.hours.ago}
is also possible). Stashes may also be referenced by specifying just the stash index (e.g. the integer n
is equivalent to stash@{n}
).
git stash pop //取出栈顶的暂存信息 git stash list //查看暂存区的所有暂存修改 git stash apply stash@{X} //取出相应的暂存;不会向pop一样从栈中移除stash git stash drop stash@{X} //将记录列表中取出的对应暂存记录删除
获取更多干货分享,欢迎【扫码关注】~