第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
# afc38c96c82ea65991322a5d28995b0851ff7edd
  • Lv2

基于第一个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 HEADgit diff-index --cached HEADgit 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

重要Lv2工具git merge-file(无需git dir也无需worktree):

在遇到冲突的情况下,标记出来手工解决:

在遇到冲突的情况下,自动解决:

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 <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 [--aggressive] <tree-ish-A> <tree-ish-C> <tree-ish-B>的意思是:

  • 清空index

  • stage数=1,读取<tree-ish-A>至index

  • stage数=2,读取<tree-ish-C>至index

  • stage数=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-indexgit 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

为了减少花在查找原始版本(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_HEAD MERGE_MODE MERGE_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_HEAD MERGE_MODE MERGE_MSG三个文件

git merge --no-ff --no-commit - ours实际上就是

  • 关于index,什么也不做

  • 在git dir中生成MERGE_HEAD MERGE_MODE MERGE_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 B

      • git merge-tree C A B

      • git read-tree -m H M

      • git read-tree -m A C 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

    • Lv4

      • git mf

      • git mnf

      • git mnfnc

    • Lv5

      • git-mnfss

最后更新于

这有帮助吗?