第6章:直接操纵merge
基础知识
从物理结构上讲,一个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
# afc38c96c82ea65991322a5d28995b0851ff7eddLv2
基于第一个tree-ish,查看第二个tree-ish的修改:
基于tree-ish,查看index的修改:
基于tree-ish,查看worktree的修改:
基于index,查看worktree的修改:
Lv3
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意味着还要列出新增了哪些文件
Lv4:
git st
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。
打包修改:
解包修改至worktree:
解包修改至index:
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,已知原始版本,如何把两人独自进行的修改在文件层面上整合?(不涉及文件内容)
git merge-tree C A B
同一个tree,已知原始版本,对方修改以后,如何把我尚未完善的修改建立在别人的基础上?
git read-tree -m H M
同一个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
git merge-file重要Lv2工具git merge-file(无需git dir也无需worktree):
在遇到冲突的情况下,标记出来手工解决:
在遇到冲突的情况下,自动解决:
tree层面:git merge-tree
git merge-tree在开始之前,先创建几个对象:
git merge-tree C A B的意义是(C+(B-A))-C,参见以下示例
当同时存在tree层面和文件层面的修改时,参见以下示例
git read-tree -m的Two Tree Merge
git read-tree -m的Two Tree Mergegit read-tree -m <tree-ish-H> <tree-ish-M>试图执行index=index+(M-H)。 对于每一个文件,考虑到H、M、index、worktree四处的状态,有22种具体情况,具体处置方法如下表所示(0表示不存在,不同小写字母表示不同版本)(摘自man git-read-tree):
编号
worktree
index
H
M
处置
0
.
0
0
0
0
keep
1
.
0
0
m
index = m
2
.
0
h
0
index = 0
3a
.
0
h
h
SEE NOTE
3b
.
0
h
m
fail
4
i
i
0
0
keep
5
w
i
0
0
keep
6
m
m
0
m
keep
7
w
m
0
m
keep
8
i
i
0
m
fail
9
w
i
0
m
fail
10
h
h
h
0
index = 0
11
w
h
h
0
fail
12
i
i
h
0
fail
13a
h
i
h
0
fail
13b
w
i
h
0
fail
14
h
h
h
h
keep
15
w
h
h
h
keep
16
i
i
h
m
fail
17a
h
i
h
m
fail
17b
m
i
h
m
fail
17c
w
i
h
m
fail
18
m
m
h
m
keep
19a
h
m
h
m
keep
19b
w
m
h
m
keep
20
h
h
h
m
index = m
21a
m
h
h
m
fail
21b
w
h
h
m
fail
对于3a,如果index全空,则执行index=m;若index非空,则keep。
举例如下:
git read-tree -m的3-Way Merge
git read-tree -m的3-Way Mergegit read-tree -m [--aggressive] <tree-ish-A> <tree-ish-C> <tree-ish-B>的意思是:
清空index
stage数=1,读取
<tree-ish-A>至indexstage数=2,读取
<tree-ish-C>至indexstage数=3,读取
<tree-ish-B>至index按下表变换(表中use表示删掉stage123的,创建一个新的stage0条目),若不能变换则不处理
--aggressive
stage1
stage2
stage3
操作
any
a
a
a
use a
any
a
x
x
use x
any
a
a
b
use b
any
a
c
a
use c
required
a
0
a
use 0
required
a
a
0
use 0
required
a
0
0
use 0
required
0
x
x
use x
举例如下:
git merge-index和git merge-one-file
git merge-index和git merge-one-file前述git read-tree只解决了最最简单的冲突。为了解决更多冲突,要么手工编辑好再git update-index或者git add,要么采用自动化工具。
git merge-index负责调用其他工具:
一种(没有卵用的)操作是调用自带工具git-merge-one-file:
注意:若提示错误 fatal: unable to read blob object e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 则执行 printf '' | git hash-object --stdin -w
问题:在使用外部工具的情况下,有没有更好的解决冲突的办法?
回答:有,即著名的recursive merge。但是由于该算法非常复杂,没有Lv2命令。此处不介绍Lv1实现,而只介绍Lv3的使用方法。
原始版本的选择:git merge-base
git merge-base为了减少花在查找原始版本(A)的努力,git merge-base -a <commit>*可以直接计算出多个commit的极近公共祖先(在偏序关系下没有“最”,只能有“极”)。
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源码)
(需要手工指定C)
先执行
git read-tree -u -m --aggressive A C B再执行
git merge-index -o git-merge-one-file -a在git dir中生成
MERGE_HEADMERGE_MODEMERGE_MSG三个文件
git merge -s octopus --no-ff --no-commit实际上就是 (参见git-merge-octopus源码)
用
git merge-base算出C先执行
git read-tree -u -m --aggressive A C B再执行
git merge-index -o git-merge-one-file -a对其他分支重复以上过程
在git dir中生成
MERGE_HEADMERGE_MODEMERGE_MSG三个文件
git merge --no-ff --no-commit - ours实际上就是
关于index,什么也不做
在git dir中生成
MERGE_HEADMERGE_MODEMERGE_MSG三个文件
git merge --no-ff --no-commit -s recursive的基本思路是 (参见man git-merge)
先用
git merge-base --all找到所有可能的C把这些C先行merge一下得到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:
其中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)。
采用以下脚本即可解决:
关于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:
然后进行merge:
注意观察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>git diff -- <path>git status
Lv4
git st
合并修改
Lv2
git merge-file [--ours|--theirs|--union] C A Bgit merge-tree C A Bgit read-tree -m H Mgit read-tree -m A C Bgit merge-index -o git-merge-one-file -a
Lv3
git merge -s resolve [--no-ff] [--no-commit] Bgit 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] Bgit merge -s subtree [--no-ff] [--no-commit] B
Lv4
git mfgit mnfgit mnfnc
Lv5
git-mnfss
最后更新于
这有帮助吗?