.git 目录结构
|── HEAD|── branches // 分支|── config // 配置|── description // 项目的描述|── hooks // 钩子| |── pre-commit.sample| |── pre-push.sample| └── ...|── info| └── exclude // 类似.gitignore 用于排除文件|── objects // 存储了blob,tree,commit对象| |── info| └── pack // 用于优化仓库体积,通过patch的方式└── refs |── heads└── tags // 标签
复制代码
blob,tree,commit对象
blob
blob对象是文件内容的快照
$ git cat-file -t e0f5c6
blob$ git cat-file -p e0f5c6
reademe
复制代码
tree
tree对象描述了工作目录,每个节点指向对应的blob或者子tree
$ git cat-file -t 443322
tree$ git cat-file -p 443322
100644 blob 723ef36f4e4f32c4560383aa5987c575a30c6535 .gitignore
100644 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de 1
100644 blob d218c7660f5672293d2b2241741f2e3f25008b9e 2
040000 tree 74080098daf8a1fa7368c2feac12cfab0e648d02 3
100644 blob e0f5c6d282792ef63ea012f200f5d7749b084fa0 README.md
复制代码
commit
commit对象是对tree的封装
$ git cat-file -t 186f17807d
commit$ git cat-file -p 186f17807d
tree 4433224cb7cbb72dae00b5138c8961522c531707
parent 45d0db32885c40a8c3244fa6ec24df2d7a631a3c
author 孙健 <jian.sun@ymm56.com> 1535621955 +0800
committer 孙健 <jian.sun@ymm56.com> 1535621955 +0800
复制代码
扩展 - 手动创建 commit
mktree // 从标准格式文本中创建一个树read-tree // 从仓库中读取到 index 文件ls-files -s // 检查当前 index 文件的结构write-tree // 通过这个 index 在仓库中创建一个树commit-tree // 将一个 tree 包装为 commit 对象-p 指定父commit-m 添加描述branch -f master HEAD // 更改分支指向
复制代码
工作区和暂存区
工作区
工作区就是我们的工作目录
暂存区
暂存区类似一个 tree 对象
$ git ls-files -s
100644 723ef36f4e4f32c4560383aa5987c575a30c6535 0 .gitignore
100644 56a6051ca2b02b04ef92d5150c9ef600403cb1de 0 1
100644 d218c7660f5672293d2b2241741f2e3f25008b9e 0 2
100644 00750edc07d6415dcc07ae0351e9397b0222b7ba 0 3/3
100644 e0f5c6d282792ef63ea012f200f5d7749b084fa0 0 README.md
复制代码
当我们clone一个仓库,或者检出一个提交``的时候,此时HEAD == 暂存区 == 工作区
-
工作区修改,未添加到暂存区 - HEAD == 暂存区 != 工作区
-
工作区修改,添加到暂存区 - HEAD != 暂存区 == 工作区
-
工作区修改,添加到暂存区,提交到仓库 - HEAD == 暂存区 == 工作区 -
nothing to commit, working tree clean
add的时候做了什么
-
从文件中创建 blob
-
将 blob 写入仓库
-
更新 index
Commit的时候 做了什么
- 从 index 文件创建 tree
- 将 tree 写入仓库
- 创建一个 commit 对象将树封装起来
- 将 HEAD 作为新创建 commit 的父 commit,并更新 HEAD 未新创建的 commit
扩展:从一个 tree 更新工作区
$ git read-tree $TREE_HASH // 从一个 tree 写入到 index
$ git checkout-index -a // 从 index 检出到工作区
复制代码
分支,标签,HEAD
branch
# refs/heads/dev
47c871bb634324cfcc41e5a5affee6aa35301e03 // branch总是指向最新的提交$ git cat-file -t 47c871b
commit
复制代码
标签
# refs/tags/dev
47c871bb634324cfcc41e5a5affee6aa35301e03 // tag指向固定的提交// 同上
复制代码
HEAD
# HEAD
ref: refs/heads/dev 此时HEAD随分支前进$ git checkout HEAD^
# HEAD
47c871bb634324cfcc41e5a5affee6aa35301e03 分离HEAD,不随分支前进// 同上
复制代码
merge
merge 常用于将两条分支合并;
略过快速合并
标准的三方合并
上图:
此时,你的开发历史从一个更早的地方开始分叉开来(diverged)。 因为,master
分支所在提交并不是 iss53
分支所在提交的直接祖先,Git 不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4
和 C5
)以及这两个分支的工作祖先(C2
),做一个简单的三方合并。
我们分析一下两条分支合并的过程
- 找到两条分支对应的commit对象;
- 找到两个commit对象共同的祖先commit对象;
- 通过两个commit对象下的tree对象对比每个blob对象的差异;
- 如果不同并且其中一个blob对象与祖先相同,则默认自动合并;
- 如果不同并且都不与祖先相同
- 修改同一处地方 产生conflict,需要手动合并
- 没有修改同一处地方 自动合并
查看合并基底
$ git merge-base master iss53
// C2的HASH_ID
复制代码
合并
* master
$ git merge iss53
// 此时产生冲突$ git merge --abort // 撤销合并
$ git commit -a // 解决冲突后,提交
复制代码
查看冲突
$ git show :1:hello.rb > hello.common.rb // 祖先
$ git show :2:hello.rb > hello.ours.rb // 我
$ git show :3:hello.rb > hello.theirs.rb // 他$ git ls-files -u
复制代码
小技巧
1.有时候我们格式化文件之后,在之后的合并中会产生很多冲突,有没有办法忽略空格上的更改吗?
git merge [branch] --ignore-space-change
git merge [branch] -s recursive -X ignore-space-change-s 选择策略-x 策略选项
复制代码
2.通过revert撤销合并,后面再次merge的时候提示已经合并过了?
再次revert,或者通过新建一个相同的commit,指定其父commit;
$ git commit-tree $TREE_HASH -p $PARENT_HASH // 需要合并的 commit 的tree hash 和其parent hash
$ git branch -f $BRANCH $NEW_COMMIT_HASH // 将分支指向新的 commit
$ git merge $branch // 此时合并就没问题了
复制代码
rebase
继续上图:
首先回到两个分支最近的共同祖先,根据当前分支(也就是要进行衍合的分支 experiment)后续的历次提交对象(这里只有一个 C4),生成一系列文件补丁;
然后以基底分支(也就是主干分支master)最后一个提交对象(C3)为新的出发点,逐个应用之前准备好的补丁文件;
最后会生成一个新的合并提交对象(C4'),从而改写 experiment 的提交历史,使它成为 master 分支的直接下游
需要注意的点
-
rebase是逐步应用补丁,可能会有多个rebase阶段,每次解决冲突都需要:
$ git add . $ git rebase --continue 复制代码
-
rebase完成之后会丢失之前的对C4的指向,导致C4无法再被找到,此时C4存在于
.git/objects
中,等待下次gc被回收; -
rebase类似于多个merge过程,比如已被应用的补丁产生的新的提交会与下一个补丁进行新的三方合并;
黄金准则 - 公用分支不可作为衍合分支
上图:
交互式rebase
$ git rebase -i HEAD~6pick fb257ad9 某次提交说明
pick fb257ad9 某次提交说明
drop fb257ad9 某次提交说明# Rebase a0daba3d..fb257ad9 onto a0daba3d (1 command)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending // 修改某次commit
# s, squash <commit> = use commit, but meld into previous commit // 将commit合并到上一个commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# d, drop <commit> = remove commit // 删除某次commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
复制代码
- 如果之前错误的合并了某次提交,可以通过drop删除该提交
- 如果想修改某次提交的信息,可以将该提交对应的状态改为edit
cherry-pick - 摘樱桃
cherry-pick常用于将某些提交应用于其他的分支;
cherry-pick
可以理解为”挑拣”提交,它会获取某一个分支的单笔提交,并作为一个新的提交引入到你当前分支上。 当我们需要在本地合入其他分支的提交时,如果我们不想对整个分支进行合并,而是只想将某一次提交合入到本地当前分支上,那么就要使用cherry-pick
了。
# cherry-pick的方式与 merge 有所不同,merge 的过程相当于两个 tree 的差异对比,而cherry-pick更像是应用更改;C<---D<---E branch2/
master A<---B \F<---G<---H branch3|HEAD*** after ***C<---D<---E<---F'<---G'<---H' branch2/
master A<---B \F<---G<---H branch3|HEAD 复制代码
当我们应用某个提交的时候,实际上会通过该提交与其父提交的差异得到发生的改变,并将这些改变应用到主分支上,cherry-pick产生的三方合并,其merge-base
是该提交的父提交;
* branch2
$ git cherry-pick B...H // 三点语法,前开后闭第一次 base:B our:E their: F 产生提交 F'
第二次 base:F our:F' their: G 产生提交 G'
第三次 base:G our:G' their: H 产生提交 H'
复制代码