基础知识
从物理结构上讲,一个commit表示一个完整的版本; 但是,从逻辑结构上讲,一个commit还可以表示 相比于之前进行了哪些修改 。
本章中涉及worktree的命令会明确标出。
复制 git init --separate-git-dir "$( pwd )" ../default-tree
# Initialized empty Git repository in /root/
查看更改
在开始之前,先创建几个对象:
复制 echo '1' | git hash-object -t blob --stdin -w
# d00491fd7e5bb6fa28c517a0bb32b8b506539d4d
echo '2' | git hash-object -t blob --stdin -w
# 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f
echo '3' | git hash-object -t blob --stdin -w
# 00750edc07d6415dcc07ae0351e9397b0222b7ba
echo '4' | git hash-object -t blob --stdin -w
# b8626c4cff2849624fb67f87cd0ad72b163671ad
echo '5' | git hash-object -t blob --stdin -w
# 7ed6ff82de6bcc2a78243fc9c54d3ef5ac14da69
echo '6' | git hash-object -t blob --stdin -w
# 1e8b314962144c26d5e0e50fd29d2ca327864913
echo '7' | git hash-object -t blob --stdin -w
# 7f8f011eb73d6043d2e6db9d2c101195ae2801f2
git mktree << EOF
100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d$( printf '\t' )1.txt
100755 blob 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f$( printf '\t' )2.txt
EOF
# a237e8338c09e7d1b2f9749f73f4f583f19fc626
git mktree << EOF
100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d$( printf '\t' )1.txt
100755 blob 00750edc07d6415dcc07ae0351e9397b0222b7ba$( printf '\t' )3.txt
EOF
# aa250e2798646facc12686e4403ccadbf1565d51
git hash-object -t commit --stdin -w << EOF
tree a237e8338c09e7d1b2f9749f73f4f583f19fc626
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
1=1 2=2
EOF
# 4cfe841426d0435270d049625a766130c108f4c8
git hash-object -t commit --stdin -w << EOF
tree aa250e2798646facc12686e4403ccadbf1565d51
parent 4cfe841426d0435270d049625a766130c108f4c8
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
1=1 3=3
EOF
# afc38c96c82ea65991322a5d28995b0851ff7edd
基于第一个tree-ish,查看第二个tree-ish的修改:
复制 git diff-tree a237 aa25
# :100755 000000 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 0000000000000000000000000000000000000000 D 2.txt
# :000000 100755 0000000000000000000000000000000000000000 00750edc07d6415dcc07ae0351e9397b0222b7ba A 3.txt
git diff-tree a237 aa25 -- 2.txt
# :100755 000000 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 0000000000000000000000000000000000000000 D 2.txt
git diff-tree afc3
# afc38c96c82ea65991322a5d28995b0851ff7edd
# :100755 000000 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 0000000000000000000000000000000000000000 D 2.txt
# :000000 100755 0000000000000000000000000000000000000000 00750edc07d6415dcc07ae0351e9397b0222b7ba A 3.txt
基于tree-ish,查看index的修改:
复制 git read-tree a237
git diff-index --cached aa25
# :000000 100755 0000000000000000000000000000000000000000 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f A 2.txt
# :100755 000000 00750edc07d6415dcc07ae0351e9397b0222b7ba 0000000000000000000000000000000000000000 D 3.txt
基于tree-ish,查看worktree的修改:
复制 rm -rf ../default-tree/*
git --work-tree=../default-tree diff-index aa25
# :100644 000000 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0000000000000000000000000000000000000000 D 1.txt
# :100755 000000 00750edc07d6415dcc07ae0351e9397b0222b7ba 0000000000000000000000000000000000000000 D 3.txt
基于index,查看worktree的修改:
复制 git --work-tree=../default-tree diff-files
# :100644 000000 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0000000000000000000000000000000000000000 D 1.txt
# :100755 000000 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 0000000000000000000000000000000000000000 D 2.txt
git diff <tree-ish> <tree-ish> -- [<path>]
相当于 git diff-tree -p <tree-ish> <tree-ish> -- [path]
git show <commit-ish> -- [<path>]
相当于 git diff-tree -p <commit-ish> -- [<path>]
git diff [<tree-ish>] -- [<path>]
相当于 git diff-index -p [<tree-ish>] -- [<path>]
git diff --cached [<tree-ish>] -- [<path>]
相当于 git diff-index -p --cached [<tree-ish>] -- [<path>]
git diff -- <path>
相当于 git diff-files -p <path>
git status
相当于 git diff-index HEAD
、git diff-index --cached HEAD
、git clean -nd
注意这里没有-p
,意味着仅仅列出哪些文件发生了变化,但并不列出具体差异
git clean -nd
意味着还要列出新增了哪些文件
git status -sb
,比起git status
要简明扼要一些。
处理修改
类似于git bundle create
将若干对象打包成字节流以便离线传送,git diff-* -p|--patch
将修改打包成字节流以便离线传送。 类似于git bundle unbundle
将字节流解包成对象,git apply
将字节流解包出修改。 需要注意的是,--patch
产生的是human-readable data,但git bundle
是machine-readable only。
打包修改:
复制 git diff-tree --patch a237 aa25 | tee ../the.patch
# diff --git a/2.txt b/2.txt
# deleted file mode 100755
# index 0cfbf08..0000000
# --- a/2.txt
# +++ /dev/null
# @@ -1 +0,0 @@
# -2
# diff --git a/3.txt b/3.txt
# new file mode 100755
# index 0000000..00750ed
# --- /dev/null
# +++ b/3.txt
# @@ -0,0 +1 @@
# +3
解包修改至worktree:
复制 rm -rf ../default-tree/*
git read-tree a237
git --work-tree=../default-tree checkout-index -fu -a
ls ../default-tree
# 1.txt
# 2.txt
git -C ../default-tree apply ../the.patch
ls ../default-tree
# 1.txt
# 3.txt
git ls-files -s
# 100644 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0 1.txt
# 100755 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 0 2.txt
解包修改至index:
复制 git ls-files -s
# 100644 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0 1.txt
# 100755 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 0 2.txt
git apply --cached ../the.patch
git ls-files -s
# 100644 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0 1.txt
# 100755 00750edc07d6415dcc07ae0351e9397b0222b7ba 0 3.txt
ls ../default-tree
# 1.txt
# 3.txt
Merge相关概念简介
Merge是git里面最为复杂也最为重要的部分。 在具体讲解每一个命令之前,先看一下Lv2的big picture:
同一份文件,已知原始版本,如何把两人独自进行的修改整合?
出现冲突时以我方为准:git merge-file --ours C A B
出现冲突时以对方为准:git merge-file --theirs C A B
出现冲突时串联起来:git merge-file --union C A B
出现冲突时做出标记,手工解决:git merge-file C A B
同一个tree,已知原始版本,如何把两人独自进行的修改在文件层面上整合?(不涉及文件内容)
同一个tree,已知原始版本,对方修改以后,如何把我尚未完善的修改建立在别人的基础上?
同一个tree,已知原始版本,如何把两人独自进行的修改整合?
先解决最简单的冲突,保留复杂冲突的完整信息:git read-tree -m A C B
试着解决最简单的几种冲突:git merge-index git-merge-one-file
同一个tree,已知原始版本,如何把多人独自进行的修改整合?
多次执行git read-tree -m A C B
即可
3-Way merge基本概念之文件层面的git merge-file
重要Lv2工具git merge-file
(无需git dir也无需worktree):
复制 cat > fileA << EOF
lineB
...some stuff...
lineC
EOF
cat > fileB << EOF
lineBB
...some stuff...
lineC
EOF
cat > fileC << EOF
lineB
...some stuff...
lineCC
EOF
cat > fileD << EOF
lineBD
...some stuff...
lineC
EOF
# 计算 fileC + (fileB - fileA)
git merge-file --stdout fileC fileA fileB
# lineBB
# ...some stuff...
# lineCC
在遇到冲突的情况下,标记出来手工解决:
复制 git merge-file --stdout fileD fileA fileB
# <<<<<<< fileD
# lineBD
# =======
# lineBB
# >>>>>>> fileB
# ...some stuff...
# lineC
git merge-file --stdout --diff3 fileD fileA fileB
# <<<<<<< fileD
# lineBD
# ||||||| fileA
# lineB
# =======
# lineBB
# >>>>>>> fileB
# ...some stuff...
# lineC
在遇到冲突的情况下,自动解决:
复制 git merge-file --stdout --our fileD fileA fileB
# lineBD
# ...some stuff...
# lineC
git merge-file --stdout --their fileD fileA fileB
# lineBB
# ...some stuff...
# lineC
git merge-file --stdout --union fileD fileA fileB
# lineBD
# lineBB
# ...some stuff...
# lineC
tree层面:git merge-tree
在开始之前,先创建几个对象:
复制 git mktree << EOF
100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d$( printf '\t' )1.txt
100755 blob 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f$( printf '\t' )2.txt
100755 blob b8626c4cff2849624fb67f87cd0ad72b163671ad$( printf '\t' )4.txt
EOF
# 5de99716b8dd347ce09718e5f628b8c78e656b8c
git hash-object -t commit --stdin -w << EOF
tree 5de99716b8dd347ce09718e5f628b8c78e656b8c
parent 4cfe841426d0435270d049625a766130c108f4c8
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
1=1 2=2 4=4
EOF
# d2b78d09d49d1f9b4ae260cf98657de9a2fedaa6
git merge-tree C A B
的意义是(C+(B-A))-C
,参见以下示例
复制 git ls-tree a237
# 100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 1.txt
# 100755 blob 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 2.txt
git ls-tree aa25
# 100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 1.txt
# 100755 blob 00750edc07d6415dcc07ae0351e9397b0222b7ba 3.txt
git ls-tree 5de9
# 100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 1.txt
# 100755 blob 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 2.txt
# 100755 blob b8626c4cff2849624fb67f87cd0ad72b163671ad 4.txt
git merge-tree 5de9 a237 aa25
# removed in remote
# base 100755 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 2.txt
# our 100755 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 2.txt
# @@ -1 +0,0 @@
# -2
# added in remote
# their 100755 00750edc07d6415dcc07ae0351e9397b0222b7ba 3.txt
# @@ -0,0 +1 @@
# +3
git merge-tree 5de9 aa25 a237
git merge-tree a237 aa25 5de9
# added in remote
# their 100755 b8626c4cff2849624fb67f87cd0ad72b163671ad 4.txt
# @@ -0,0 +1 @@
# +4
当同时存在tree层面和文件层面的修改时,参见以下示例
复制 # 注意:3.txt包含的是"4"
git mktree << EOF
100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d$( printf '\t' )1.txt
100755 blob b8626c4cff2849624fb67f87cd0ad72b163671ad$( printf '\t' )3.txt
EOF
# 47e3b7857c03c35eae515b36fe3828ef073cc2aa
# merge-tree产生非常规输出(removed in local),暗示需要处理冲突
git merge-tree 47e3 a237 aa25
# removed in local
# base 100755 b8626c4cff2849624fb67f87cd0ad72b163671ad 3.txt
# their 100755 00750edc07d6415dcc07ae0351e9397b0222b7ba 3.txt
git read-tree -m
的Two Tree Merge
git read-tree -m <tree-ish-H> <tree-ish-M>
试图执行index=index+(M-H)
。 对于每一个文件,考虑到H、M、index、worktree四处的状态,有22种具体情况,具体处置方法如下表所示(0表示不存在,不同小写字母表示不同版本)(摘自man git-read-tree
):
对于3a,如果index全空,则执行index=m;若index非空,则keep。
举例如下:
复制 rm -rf ../default-tree/*
git read-tree 5de9
# 此处要加-u,来确保index和worktree在stat意义下一致
git --work-tree=../default-tree checkout-index -fu -a
git ls-files -s
# 100644 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0 1.txt
# 100755 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 0 2.txt
# 100755 b8626c4cff2849624fb67f87cd0ad72b163671ad 0 4.txt
ls ../default-tree
# 1.txt
# 2.txt
# 4.txt
git ls-tree a237
# 100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 1.txt
# 100755 blob 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 2.txt
git ls-tree aa25
# 100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 1.txt
# 100755 blob 00750edc07d6415dcc07ae0351e9397b0222b7ba 3.txt
git --work-tree=../default-tree read-tree -m a237 aa25
git ls-files -s
# 100644 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0 1.txt
# 100755 00750edc07d6415dcc07ae0351e9397b0222b7ba 0 3.txt
# 100755 b8626c4cff2849624fb67f87cd0ad72b163671ad 0 4.txt
ls ../default-tree
# 1.txt
# 2.txt
# 4.txt
# 总结:
# 1.txt hhhh -> keep (#14)
# 2.txt hhh0 -> index = 0 (#10)
# 3.txt 000m -> index = m (#1)
# 4.txt ii00 -> keep (#4)
git read-tree -m
的3-Way Merge
git read-tree -m [--aggressive] <tree-ish-A> <tree-ish-C> <tree-ish-B>
的意思是:
stage数=1,读取<tree-ish-A>
至index
stage数=2,读取<tree-ish-C>
至index
stage数=3,读取<tree-ish-B>
至index
按下表变换(表中use表示删掉stage123的,创建一个新的stage0条目),若不能变换则不处理
举例如下:
复制 git ls-tree a237
# 100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 1.txt
# 100755 blob 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 2.txt
git ls-tree 47e3
# 100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 1.txt
# 100755 blob b8626c4cff2849624fb67f87cd0ad72b163671ad 3.txt
git ls-tree aa25
# 100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 1.txt
# 100755 blob 00750edc07d6415dcc07ae0351e9397b0222b7ba 3.txt
rm -rf ../default-tree/*
git read-tree 47e3
git --work-tree=../default-tree checkout-index -fu -a
git --work-tree=../default-tree read-tree -m a237 47e3 aa25
# 此时可以看见非0的stage数
git ls-files -s
# 100644 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0 1.txt
# 100755 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 1 2.txt
# 100755 b8626c4cff2849624fb67f87cd0ad72b163671ad 2 3.txt
# 100755 00750edc07d6415dcc07ae0351e9397b0222b7ba 3 3.txt
# 总结:
# 1.txt aaa, collapse
# 2.txt a00, non-collapse
# 3.txt 0cb, non-collapse
# 此时无法write-tree:
git write-tree
# 2.txt: unmerged (0cfbf08886fca9a91cb753ec8734c84fcbe52c9f)
# 3.txt: unmerged (b8626c4cff2849624fb67f87cd0ad72b163671ad)
# 3.txt: unmerged (00750edc07d6415dcc07ae0351e9397b0222b7ba)
# fatal: git-write-tree: error building trees
git --work-tree=../default-tree checkout-index --stage=all -f --all
# .merge_file_bL417W . . 2.txt
# . .merge_file_m91Yo2 .merge_file_FwwWF7 3.txt
git merge-index
和git merge-one-file
前述git read-tree
只解决了最最简单的冲突。为了解决更多冲突,要么手工编辑好再git update-index
或者git add
,要么采用自动化工具。
git merge-index
负责调用其他工具:
复制 # 注意空格
git merge-index echo -a
# 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f 2.txt 100755
# b8626c4cff2849624fb67f87cd0ad72b163671ad 00750edc07d6415dcc07ae0351e9397b0222b7ba 3.txt 100755 100755
一种(没有卵用的)操作是调用自带工具git-merge-one-file
:
复制 rm -rf ../default-tree/*
git read-tree 47e3
git --work-tree=../default-tree checkout-index -fu -a
git --work-tree=../default-tree read-tree -m --aggressive a237 47e3 aa25
git ls-files -s
# 100644 d00491fd7e5bb6fa28c517a0bb32b8b506539d4d 0 1.txt
# 100755 b8626c4cff2849624fb67f87cd0ad72b163671ad 2 3.txt
# 100755 00750edc07d6415dcc07ae0351e9397b0222b7ba 3 3.txt
# 这个自带工具非常弱,解决不了什么问题,真心不比--aggressive强多少
git --work-tree=../default-tree merge-index git-merge-one-file -a
# fatal: unable to read blob object e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
# error: Could not stat : No such file or directory
# ERROR: content conflict in 3.txt
# fatal: merge program failed
# Added 3.txt in both, but differently.
注意:若提示错误 fatal: unable to read blob object e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
则执行 printf '' | git hash-object --stdin -w
问题:在使用外部工具的情况下,有没有更好的解决冲突的办法?
回答:有,即著名的recursive merge。但是由于该算法非常复杂,没有Lv2命令。此处不介绍Lv1实现,而只介绍Lv3的使用方法。
原始版本的选择:git merge-base
为了减少花在查找原始版本(A)的努力,git merge-base -a <commit>*
可以直接计算出多个commit的极近公共祖先(在偏序关系下没有“最”,只能有“极”)。
复制 git merge-base -a afc3 d2b7
# 4cfe841426d0435270d049625a766130c108f4c8
Lv3
git merge -s <strategy>
(除了recursive、subtree、ours以外)实际上就是调用git merge-<strategy>
。
而所谓的subtree merge,本质上就是subtree shift非空的recursive merge。 git会自动计算给B添加的prefix,但也可以通过-Xsubtree=
来手动指定。
git merge -s resolve --no-ff --no-commit
实际上就是 (参见git-merge-resolve
源码 )
先执行git read-tree -u -m --aggressive A C B
再执行git merge-index -o git-merge-one-file -a
在git dir中生成MERGE_HEAD
MERGE_MODE
MERGE_MSG
三个文件
git merge -s octopus --no-ff --no-commit
实际上就是 (参见git-merge-octopus
源码 )
先执行git read-tree -u -m --aggressive A C B
再执行git merge-index -o git-merge-one-file -a
在git dir中生成MERGE_HEAD
MERGE_MODE
MERGE_MSG
三个文件
git merge --no-ff --no-commit - ours
实际上就是
在git dir中生成MERGE_HEAD
MERGE_MODE
MERGE_MSG
三个文件
git merge --no-ff --no-commit -s recursive
的基本思路是 (参见man git-merge
)
先用git merge-base --all
找到所有可能的C
考虑重命名,对A C' B进行3-way merge
关于recursive,还有很多的参数可以调节,比如
-Xours
类似于git merge-file --ours
-Xtheirs
类似于git merge-file -theirs
-Xsubtree
指定给B加什么前缀再进行recursive merge
若有--ff
则表明:若A=C或A=B,则不做任何事情。
若有--no-commit
则表示:若无冲突,则自动执行git commit
需要注意的是,在MERGE_HEAD
等文件存在时,git commit
调用git commit-tree
时会自动带上几个-p
参数,生成多个parent的commit,其中第1个parent是HEAD,余下的是MERGE_HEAD
的内容,也即git merge
的参数。
Lv4
由于很多时候我们希望主动控制要不要创建merge commit(也即手动决定--ff-only
或者--no-ff
,且希望在commit之前仔细检查合并是否正确(如跑单元测试),故本文建议使用如下alias:
复制 alias.mf=merge --ff-only
alias.mnf=merge --no-ff
alias.mnfnc=merge --no-ff --no-commit
其中git mf
用于git fetch
之后,git mnf
用于日常merge其他简单分支,而git mnfnc
用于尝试merge(也即git read-tree -u -m
)、跑单元测试再commit的情况。
Lv5
有一类merge情况是,需要用其他分支 完全取代 当前分支的某一目录。(第12章整章建立在此基础之上) 然而,即便git merge --no-ff -s subtree -Xsubtree=<prefix>
有时也会出错(毕竟是git read-tree -m
)。
采用以下脚本即可解决:
复制 git-mnfss () {
git rm --cached -r -- $1
git read-tree --prefix $1 / $1
git checkout-index -fua
git clean -f -- $1
git reset --soft $( echo "Merge branch $1" | git commit-tree $( git write-tree ) -p HEAD -p $1)
}
关于merge信息的完整性
在执行git merge --no-ff B*
的时候,新创建的commit中包括多个parent,用来记录谁和谁进行了merge。 这是为了能够在以后追溯到当初merge时候的细节。 然而受限于parent的格式,parent只能记录哪些commit进行了merge,与commit相关的其他信息就无法记录了: 附于该commit上的note,指向该commit的tag(s),以及该commit所属的refs。
对于notes,可以直接忽略,因为note里面的信息几乎都是本机使用
对于tag,存储于新创建的commit的message body中
对于带有签名(见第13章)的tag,存储于新创建的commit的mergetag中
对于ref,存储于message中(Merge xxx branch)
首先创建若干空commit和tag:
复制 printf '' | git mktree
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904
git hash-object -t commit --stdin -w << EOF
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
A
EOF
# 6784b23b1a03700628d8adb65b57b5b4816caa01
echo $'100644 blob 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f\tB.txt' | git mktree
# 6f707a369aa805c91f6d737ccda037d137d0ef93
git hash-object -t commit --stdin -w << EOF
tree 6f707a369aa805c91f6d737ccda037d137d0ef93
parent 6784b23b1a03700628d8adb65b57b5b4816caa01
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
B
EOF
# f1d113e4db427a1824524d17928a2cb53cd5090a
echo $'100644 blob 00750edc07d6415dcc07ae0351e9397b0222b7ba\tC.txt' | git mktree
# f6ab164f6371575aff1ce5c21441835e5568b182
git hash-object -t commit --stdin -w << EOF
tree f6ab164f6371575aff1ce5c21441835e5568b182
parent 6784b23b1a03700628d8adb65b57b5b4816caa01
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
C
EOF
# 28c0a4a3bab80a464dd384cf4e3d2b83cceb602b
git branch br-C 28c0
echo $'100644 blob b8626c4cff2849624fb67f87cd0ad72b163671ad\tD.txt' | git mktree
# 2cff17f83cfc5f0cd3af03f99257e35906e6fb7c
git hash-object -t commit --stdin -w << EOF
tree 2cff17f83cfc5f0cd3af03f99257e35906e6fb7c
parent 6784b23b1a03700628d8adb65b57b5b4816caa01
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
D
EOF
# 7f24235935c56e397d2d1d55bb470fe1b01b8209
git tag tag-D 7f24
echo $'100644 blob 7ed6ff82de6bcc2a78243fc9c54d3ef5ac14da69\tE.txt' | git mktree
# 4ff6131a9638bb0a38917c72255c3c0635d0f464
git hash-object -t commit --stdin -w << EOF
tree 4ff6131a9638bb0a38917c72255c3c0635d0f464
parent 6784b23b1a03700628d8adb65b57b5b4816caa01
author b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
committer b1f6c1c4 <b1f6c1c4@gmail.com> 1514736000 +0800
E
EOF
# 1a1640224e55b3a7d05108c6b91e03e6cc65ffbe
GIT_COMMITTER_NAME = b1f6c1c4 \
GIT_COMMITTER_EMAIL=b1f6c1c4@gmail.com \
GIT_COMMITTER_DATE= '1600000000 +0800' \
git tag -a -m 'Tag for E' tag-obj-E 1a16
然后进行merge:
复制 git update-ref --no-deref HEAD 6784
git -C ../default-tree reset --hard
# HEAD is now at 6784b23 A
git -C ../default-tree clean -fdx
# Removing .merge_file_FwwWF7
# Removing .merge_file_bL417W
# Removing .merge_file_m91Yo2
GIT_AUTHOR_NAME = b1f6c1c4 \
GIT_AUTHOR_EMAIL=b1f6c1c4@gmail.com \
GIT_AUTHOR_DATE= '1600000000 +0800' \
GIT_COMMITTER_NAME=b1f6c1c4 \
GIT_COMMITTER_EMAIL=b1f6c1c4@gmail.com \
GIT_COMMITTER_DATE= '1600000000 +0800' \
git -C ../default-tree merge --no-ff tag-obj-E f1d1 br-C tag-D
# Fast-forwarding to: tag-obj-E
# Trying simple merge with f1d1
# Trying simple merge with br-C
# Trying simple merge with tag-D
# Merge made by the 'octopus' strategy.
# B.txt | 1 +
# C.txt | 1 +
# D.txt | 1 +
# E.txt | 1 +
# 4 files changed, 4 insertions(+)
# create mode 100644 B.txt
# create mode 100644 C.txt
# create mode 100644 D.txt
# create mode 100644 E.txt
git cat-file commit HEAD
# tree ae618f9e9f1a0ce0fdc25f7e4dcfdc5bc9c09c49
# parent 6784b23b1a03700628d8adb65b57b5b4816caa01
# parent 1a1640224e55b3a7d05108c6b91e03e6cc65ffbe
# parent f1d113e4db427a1824524d17928a2cb53cd5090a
# parent 28c0a4a3bab80a464dd384cf4e3d2b83cceb602b
# parent 7f24235935c56e397d2d1d55bb470fe1b01b8209
# author b1f6c1c4 <b1f6c1c4@gmail.com> 1600000000 +0800
# committer b1f6c1c4 <b1f6c1c4@gmail.com> 1600000000 +0800
#
# Merge branch 'br-C', tags 'tag-obj-E' and 'tag-D'; commit 'f1d1' into HEAD
#
# Tag for E
注意观察commit message中对于各parent的不同的描述。 另外,commit message中还包含了tag message。
总结
查看和处理修改
Lv2
git diff-tree [-p] <tree-ish> <tree-ish> -- <path>
git diff-tree [-p] <commit-ish> -- <path>
git diff-index [-p] [--cached] <tree-ish> -- <path>
git diff-files [-p] <path>
git apply [--cached] <patch> -- <path>
Lv3
git diff <tree-ish> <tree-ish> -- <path>
git show <commit-ish> -- <path>
git diff [--cached] <tree-ish> -- <path>
合并修改
Lv2
git merge-file [--ours|--theirs|--union] C A B
git merge-index -o git-merge-one-file -a
Lv3
git merge -s resolve [--no-ff] [--no-commit] B
git merge -s octopus [--no-ff] [--no-commit] B*
git merge -s ours [--no-ff] [--no-commit] B*
git merge -s recursive [--no-ff] [--no-commit] B
git merge -s subtree [--no-ff] [--no-commit] B