带你走进git-重写历史记录

一、git对象文件创建

开篇先补充一个知识点,就是比如我建立一个文件之后,使用git add就会生成一个git对象,但是git对象生成后可以在.git/objects里面对应,首先我们来初始化一个仓库git init。

1
$ git init

然后我们来创建两个文件文件名分别为a和b。

1
$ touch a b

将a文件添加到暂存区,然后再将b添加到暂存区,我们会想到这时候有两个git对象产生,但是git对象对应.git/objects文件。

1
2
3
4
5
6
7
8
$ git add .

$ find .git/objects/
.git/objects/
.git/objects/e6
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/info
.git/objects/pack

我们来查看下.git/objects文件下面会产生几条git对象库。这时候超出了我们想象,我们认为对象文件也应该创建两个但是仅仅创建了一个,这是为什么呢?那么着一个文件又指的是什么呢?

1
2
3
$ git cat-file -t e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

blob

上述命令查看了当前sha-1是git对象(blob)。这时候我们根本就看不出来这个a和b的存放位置。我们将暂存区的文件提交到记录中,git commit –m “initial commit”

1
2
3
4
5
$ git commit -m 'initial commit'
[master (root-commit) 5b077dd] initial commit
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 a
create mode 100644 b

这时候我们再来看一下.git/object文件下内容,产生了一个tree对象和commit对象,这个commit对象指向了tree对象。

1
2
3
4
5
6
7
8
9
10
$ find .git/objects/
.git/objects/
.git/objects/29
.git/objects/29/6e56023cdc034d2735fee8c0d85a659d1b07f4
.git/objects/5b
.git/objects/5b/077ddc03fa28dff8a1ac0d85969fcc2ac40c0b
.git/objects/e6
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/info
.git/objects/pack

分别查看下各自的对象类型:

1
2
3
4
5
6
$ git cat-file -t 296e56023cdc034d2735fee8c0d85a659d1b07f4
Tree
$ git cat-file -t 5b077ddc03fa28dff8a1ac0d85969fcc2ac40c0b
Commit
$ git cat-file -t e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
Blob

那么这时候我们要查看一下tree对象里面的内容,应该是a和b分别创建的git对象。

1
2
3
4
5
$ git cat-file -p 296e56023cdc034d2735fee8c0d85a659d1b07f4

100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a

100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 b

OK,我们到了这里才看到了,原来产生的两个git对象都存放在了e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391这个文件里面,接下来我要讲一下为什么会存放在一起。
首先来说一下sha-1的生成,一个将待存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和,也就是说我们创建一个文件a他会根据文件a里面的数据和头部信息拼在一起生成一个sha-1的值,这个头部信息是固定的,如下图所示:

正因为a和b文件的内容一样都是blob空文本所以生成sha-1是一样的所以会放在一个文件里面。

二、重写历史记录

以下的命令帮我们重写历史记录

  • git commit –amend可以产生一个新的提交用来替换掉当前所提交的这个commit
  • git rebase 维护线性历史
  • git reset比如说我们做了一个合并的提交我们想要撤销掉提交然历史记录跟没有产生这样的历史记录一个合并
  • git reflog它维护了一个HEAD引用的历史信息,通常配合git reset来使用

三、指令详解

  1. git commit –amend有时候我们提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有–amend 选项的提交命令尝试重新提交:
    新建两个文件a和b这时候我们只提交一个文件a到暂存区里面
1
2
touch  a b
$ git add a

提交a文件到暂存区内这时候我们就要来看一下生成的对象以及对象类型。

1
2
3
4
5
6
$ find .git/objects/
.git/objects/
.git/objects/e6
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/info
.git/objects/pack

发现生成了一个e6的文件夹和9de29bb2d1d6434b8b29ae775ad8c2e48c5391文件名的文件。

1
2
$ git cat-file -t e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
blob

查看下新生成对象的类型是git对象blob。接下来我们将a文件进行提交到记录里面去。

1
2
3
4
5
$ git commit -m 'commit file a'
[master (root-commit) 96696f0] commit file a
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 a
提交完成后,我们再来看一下.git/objects文件下面的内容。
1
2
3
4
5
6
7
8
9
10
$ find .git/objects/
.git/objects/
.git/objects/49
.git/objects/49/6d6428b9cf92981dc9495211e6e1120fb6f2ba
.git/objects/96
.git/objects/96/696f06370488cc9b271dbd870d8ba0d4e7ce3c
.git/objects/e6
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/info
.git/objects/pack

分别查看下96696f和496d64的对象类型。

1
2
3
4
5
$ git cat-file -t 496d6428b9cf92981dc9495211e6e1120fb6f2ba
tree

$ git cat-file -t 96696f06370488cc9b271dbd870d8ba0d4e7ce3c
commit

这时候确定了96696f为commit对象,那么496d64就是tree对象。查看下他们的内容。

1
2
3
4
5
6
$ git cat-file -p 96696f06370488cc9b271dbd870d8ba0d4e7ce3c
tree 496d6428b9cf92981dc9495211e6e1120fb6f2ba
author BattleHeart <dwlsxj@126.com> 1452085041 +0800
committer BattleHeart <dwlsxj@126.com> 1452085041 +0800

commit file a

commit对象内容下面是指向496d64这个tree对象,并且有作者和提交时写的提交信息。

1
2
$ git cat-file -p 496d6428b9cf92981dc9495211e6e1120fb6f2ba
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a

Tree对象内容则是他的叶子内容,因为我们只提交了a文件所以只有a文件这个内容。
这时候我们发现应该a文件和b文件一起提交而不是分开提交,这时候就是用到接下来的命令–amend命令。那么首先要做的就先把b文件放到暂存区理面

1
$ git add b

这时候再使用git commit –amend就会将暂存区的文件一起提交

1
$ git commit –amend

当我们输入的时候git commit –amend命令后我们先不着急操作,我们先来看一下产生的文件内容.git/objects文件里面。

1
2
3
4
5
$ find .git/objects/ -type f
.git/objects/29/6e56023cdc034d2735fee8c0d85a659d1b07f4
.git/objects/49/6d6428b9cf92981dc9495211e6e1120fb6f2ba
.git/objects/96/696f06370488cc9b271dbd870d8ba0d4e7ce3c
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391

会新生成一个296e56这样一个对象。那么这个对象是什么呢?我们来仔细观察下。

1
2
3
4
5
6
$ git cat-file -t 296e56023cdc034d2735fee8c0d85a659d1b07f4
tree

$ git cat-file -p 296e56023cdc034d2735fee8c0d85a659d1b07f4
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 b

啊哈,原来是个tree对象当然这里同样也产生了blob对象只是内容和a内容一样所以合并成一个文件了,因为296e56树下面有两个子节点。

我们这时候再进行填写注释,我们会发现我上一次提交的信息他给展现出来了,这里我们可以修改成first commit。当我们保存退出后就会产生如下输出信息:

1
2
3
4
5
6
$ git commit --amend
[master 433aedb] first commit
Date: Wed Jan 6 20:57:21 2016 +0800
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 a
create mode 100644 b

这回我们再来看一下.git/objects里面内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ find .git/objects/
.git/objects/
.git/objects/29
.git/objects/29/6e56023cdc034d2735fee8c0d85a659d1b07f4
.git/objects/43
.git/objects/43/3aedb274b59386535efa27c874d1ff5ded4a9b
.git/objects/49
.git/objects/49/6d6428b9cf92981dc9495211e6e1120fb6f2ba
.git/objects/96
.git/objects/96/696f06370488cc9b271dbd870d8ba0d4e7ce3c
.git/objects/e6
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/info
.git/objects/pack

多出来了一个433aed对象,那么这个对象是什么东西呢?以及里面存放的东西是什么?

1
2
3
4
5
6
7
8
9
$ git cat-file -t 433aedb274b59386535efa27c874d1ff5ded4a9b
commit

$ git cat-file -p 433aedb274b59386535efa27c874d1ff5ded4a9b
tree 296e56023cdc034d2735fee8c0d85a659d1b07f4
author BattleHeart <dwlsxj@126.com> 1452085041 +0800
committer BattleHeart <dwlsxj@126.com> 1452085549 +0800

first commit

我们看到了这个是个commit对象并且这个commit对象指向了新生成的树对象,以及commit对象没有父节点也就是说本次提交相当于第一次提交。

  1. git rebase
    新建两个文件a和b文件,添加文件a的内容为this is file a,添加文件内容b为this is file b。并将其添加到暂存区内。

git add .添加到暂存区内,查看一下.git/objects里面的内容,会产生两个git对象,对象类型为blob。

1
2
3
4
5
6
7
8
9
$ git add .
$ find .git/objects/
.git/objects/
.git/objects/77
.git/objects/77/486f708f7f7a0dab6f951148b75365081fcc3c
.git/objects/ba
.git/objects/ba/f5b90e3bc9f0c775c8d52764d353212259ffb3
.git/objects/info
.git/objects/pack

查看下git对象的类型以及git对象下的内容。

1
2
3
4
5
6
7
8
9
10
11
$ git cat-file -t 77486f708f7f7a0dab6f951148b75365081fcc3c
blob

$ git cat-file -t baf5b90e3bc9f0c775c8d52764d353212259ffb3
blob

$ git cat-file -p baf5b90e3bc9f0c775c8d52764d353212259ffb3
this is file a

$ git cat-file -p 77486f708f7f7a0dab6f951148b75365081fcc3c
this is file b

将两个文件同时提交到记录里面。Git commit

1
2
3
4
5
6
7
8
9
$ git commit -m 'first commit'
[master (root-commit) 4bd85c6] first commit
warning: LF will be replaced by CRLF in a.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in b.
The file will have its original line endings in your working directory.
2 files changed, 2 insertions(+)
create mode 100644 a
create mode 100644 b

这时候我们来看一下objects文件下内容。

1
2
3
4
5
6
7
8
9
10
11
12
$ find .git/objects/
.git/objects/
.git/objects/4b
.git/objects/4b/d85c6b6ed389f268ca7c183e560fb9cb33430e
.git/objects/77
.git/objects/77/486f708f7f7a0dab6f951148b75365081fcc3c
.git/objects/ba
.git/objects/ba/f5b90e3bc9f0c775c8d52764d353212259ffb3
.git/objects/bd
.git/objects/bd/887f9b4dc0d570bd7cae7e604e0da5dd3f466a
.git/objects/info
.git/objects/pack

这时候会多两个文件。

1
2
3
4
5
$ git cat-file -t bd887f
Tree

$ git cat-file -t 4bd85c
Commit

这时候整体记录的内容结构如下图所示:

这时候我们新建一个分支test,并切换到test分支上面,在分支上面修改下a文件内容添加内容。

1
$ git branch test

这时候结构是这个样子的。

这时候我们要在master分支上面修改下a文件并且提交a文件内容,这时候会产生新的commit对象以及tree对象和新生成git对象。
首先我们要做到在master分值上面先修改下a文件然后commit到记录里面,然后在修改下b文件在提交到记录里面。这是会产生两条记录让我们来看一下:
首先做的就是添加a文件内容为this is master branch,并且添加到暂存区。

1
2
3
$ vim a

$ git add .

在来查看下新生成的git对象(blob),这个对象的sha-1为a4cb8b4(与上次对比)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ find .git/objects/
.git/objects/
.git/objects/4b
.git/objects/4b/d85c6b6ed389f268ca7c183e560fb9cb33430e
.git/objects/77
.git/objects/77/486f708f7f7a0dab6f951148b75365081fcc3c
.git/objects/a4
.git/objects/a4/cb8b4e0819ae97811f650886390e17e54bc93c
.git/objects/ba
.git/objects/ba/f5b90e3bc9f0c775c8d52764d353212259ffb3
.git/objects/bd
.git/objects/bd/887f9b4dc0d570bd7cae7e604e0da5dd3f466a
.git/objects/info
.git/objects/pack

查看下a4cb8b4类型是blob那么查看下当前文件下的内容正是我们添加的内容一致。

1
2
3
4
5
6
$ git cat-file -t a4cb8b4
blob

$ git cat-file -p a4cb8b4
this is file a
this is master branch

这时候提交到版本控制里面去,将修改的文件a。

1
2
3
4
5
6
7
$ git commit -m 'master commit'
[master warning: LF will be replaced by CRLF in a.
The file will have its original line endings in your working directory.
a6bcbb7] master commit
warning: LF will be replaced by CRLF in a.
The file will have its original line endings in your working directory.
1 file changed, 1 insertion(+)

当然提交上去的时候这时候必然会产生commit对象和新的tree对象。让我们看一下谁是commit对象,谁是tree对象。

1
2
3
4
5
6
7
8
$ find .git/objects/ -type f
.git/objects/4b/d85c6b6ed389f268ca7c183e560fb9cb33430e
.git/objects/77/486f708f7f7a0dab6f951148b75365081fcc3c
.git/objects/a4/cb8b4e0819ae97811f650886390e17e54bc93c
.git/objects/ba/f5b90e3bc9f0c775c8d52764d353212259ffb3
.git/objects/bd/887f9b4dc0d570bd7cae7e604e0da5dd3f466a
.git/objects/99/1f631d3674f37a8595fd782ce9fd57d354103a
.git/objects/a6/bcbb77ada043da6df0a55918cac224bae50ef1

这时候我们发现比上一次对象多出了两个对象分别是991f63和a6bcbb,多出了这两个对象,那么我们来看一下这两个对象就是是什么类型的。

1
2
3
4
5
6
$ git cat-file -t 991f63
tree

$ git cat-file -p 991f63
100644 blob a4cb8b4e0819ae97811f650886390e17e54bc93c a
100644 blob 77486f708f7f7a0dab6f951148b75365081fcc3c b

OK,991f63是tree对象,而且tree对象下面的内容中有一个是我们刚修改的a文件的git对象和第一次提交的b文件的git对象。
那么a6bcbb就是commit对象,且commit对象的指向是上面的tree对象。

1
2
3
4
5
6
7
8
9
$ git cat-file -t a6bcbb
commit

$ git cat-file -p a6bcbb
tree 991f631d3674f37a8595fd782ce9fd57d354103a
parent 4bd85c6b6ed389f268ca7c183e560fb9cb33430e
author BattleHeart <dwlsxj@126.com> 1452165520 +0800
committer BattleHeart <dwlsxj@126.com> 1452165520 +0800
master commit

Ok,这时候整体的提交之后的示意图如下所示:

这时候按照上面步骤看一下b的,这时候git对象变成了af379ed,commit对象变成了359118,tree对象变成了73f2e9。下面是操作记录:

这时候生成commit结构图:

接下来就要换做另一个分支了,按照上面的步骤从新来一遍,先修改下a文件,然后提交到暂存区,然后在修改下b文件再提交到暂存区,这里演示和分析过程就不做了。偷个懒,因为和上面分析过程是一样的不在这里做重复说明了,这时候我们会看到一个分支,分支的commit提交记录是这样的。

详细的结构图我也画了一份出来了,这样方便我们对原文本的追踪以及对整体原理的理解,通过分析哪些对象是新生成的那些对象是原有的就能分析出整体的结构走向。下面就上整体详细的分析图。

这时候我们会发现有两个分支出来了,因为我们现在test分支上面,我们使用git rebase master命令来维护一个线性的记录。
当我是用vim的时候肯定会产生冲突,这时候我们解决下冲突文件a和b文件再调用git rebase –continue命令即可,这里要详细讲解下合成的原理以及内容,当我git rebase master的时候会产生两个树对象和一个git对象出来,这时候我们的HEAD头部将会切换到master分支上面,也就是说接下来的操作是在master分支指向的最后一个分支上面进行与test分支上面每一个进行合并,这里我们来看一下git对象的内容是:这个树对象的sha-1是:1de9f3f3cb3c87aaf2e43732ae13bf2aff1f8cda,我们会发现这个tree文件下面git对象是我们之前创建的,并不是这一次产生的,这个git对象是cce4bf,我们查看下他的内容如下所示:(这tree对象下面blob是test分支最后生成的内容)

1
2
3
$ git cat-file -p cce4bf
this is file a
this is test branch (file a)

而产生的第二个tree对象是3ada29384374b5219c3564ce1fa999ac4fde99aa是这个,我们发现他的git对象子节点也是之前创建的baf5b9,这个对象的内容是:(这个tree对象下面的blob是master分支第一次产生的内容)

1
2
$ git cat-file -p baf5b9
this is file a

这时候我们会发现还有一个git对象blob,这个git对象里面到底是什么东西呢?这会引起我们很大的想象,为什么会产生上面两个tree并指向了之前的git对象呢?下面揭开谜底,原来新生成的blob对象就是a文件产生的冲突文件,文件的sha-1是:

1
2
3
4
5
6
7
8
.git/objects/72/038d17664a165e8a0bc65b9b028f3322e7f418 blob
$ git cat-file -p 72038d
this is file a
<<<<<<< HEAD
this is master branch
=======
this is test branch (file a)
>>>>>>> this is commit on test branch

这个文件是怎么产生的,这要说一下冲突产生的算法,我喜欢用数学的思维方式思考:给定两个提交 A和 B,合并提交(commit)操作 A∨B 就可以描述为: [A∨B ]=[ A]+[ B]−[ C ] 这里的 C是A 和 B的合并共有项(最近提交树祖先共同含有的部分),我们必须要“减去” C,因为如果不这样的话,我们就会有两个A∧B。也就是第一个树对象是B也就是不是HEAD指针指向的master分支下的a的内容,HEAD指针下的内容就是A,内容如下:

1
2
this is file a
this is master branch

A和B的共同之处就是this is file a这个内容(C),A+B-C就是我们现在看到冲突的文件blob(72038d)
这时候我们解决完冲突时候,git add . 之后就会生成一个新的blob对象就是这个对象。

1
2
3
4
.git/objects/07/cf0edd220a022c7447a8cf111bac3ed3422db9  blob对象
this is file a
this is master branch
this is test branch (file a)

这个对象我们在调用git rebase –continue的时候就会产生新的commit对象和tree对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.git/objects/77/14fb6ec6a19ba0d546d1cd10f89261445d3553  commit对象

$ git cat-file -p 7714fb
tree 000fbac4af12a11f969d5e46d86d135f62adff8b
parent 35911829097e90763845733d090e17c32d9a218c
author BattleHeart <dwlsxj@126.com> 1452168533 +0800
committer BattleHeart <dwlsxj@126.com> 1452169355 +0800

this is commit on test branch

.git/objects/00/0fbac4af12a11f969d5e46d86d135f62adff8b tree对象
Administrator@USER-20150201IC MINGW64 ~/Desktop/test (test)
$ git cat-file -p 000fba
100644 blob 07cf0edd220a022c7447a8cf111bac3ed3422db9 a
100644 blob af379ed27a46022aca5a851bc55a8f69fd7afe3a b

这里我们会发现b文件af379e是HEAD指针的指向的master分支的b文件的内容。

1
2
3
4
.git/objects/af/379ed27a46022aca5a851bc55a8f69fd7afe3a blob
$ git cat-file -p af379e
this is file b
this is branch master(b)

这时候我们在进行第二次处理冲突内容,也就是b文件的内容,这时候产生的内容和上面的一样的内容。这时候下面的内容就变成这样:(第二部分的分析请读者自行分析)一样的步骤。
结构图就变成如下的样子:

整体的commit对象的图就变成这样:(c4’和c5’都是C3的基础上进行合并的)

花絮

接下来的内容就是上图中的节点,至于中间产生的节点只有第一次使用git rebase –continue的时候的结果,第二次的结果还请读者自行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.git/objects/43be0ce752cdabca0968e721fb030000093baa4d commit对象
tree eefcff716912aa684576546f63a06f3a52ce0959
parent 7714fb6ec6a19ba0d546d1cd10f89261445d3553
author BattleHeart <dwlsxj@126.com> 1452168857 +0800
committer BattleHeart <dwlsxj@126.com> 1452169398 +0800

下面tree对象
.git/objects/eefcff716912aa684576546f63a06f3a52ce0959 tree对象
$ git cat-file -p eefcff
100644 blob 07cf0edd220a022c7447a8cf111bac3ed3422db9 a
100644 blob 19f1ba2426ee5e72b95412c2707e75d9feb2833e b

下面得内容
.git/objects/07cf0edd220a022c7447a8cf111bac3ed3422db9 blob对象在上面
tree对象里
this is file a
this is master branch
this is test branch (file a)

.git/objects/19/f1ba2426ee5e72b95412c2707e75d9feb2833e blob
this is file b
this is branch master(b)
this is test branch (file b)

第一次commit对象
.git/objects/7714fb6ec6a19ba0d546d1cd10f89261445d3553 commit对象
$ git cat-file -p 7714fb
tree 000fbac4af12a11f969d5e46d86d135f62adff8b
parent 35911829097e90763845733d090e17c32d9a218c
author BattleHeart <dwlsxj@126.com> 1452168533 +0800
committer BattleHeart <dwlsxj@126.com> 1452169355 +0800

this is commit on test branch

.git/objects/00/0fbac4af12a11f969d5e46d86d135f62adff8b tree对象
Administrator@USER-20150201IC MINGW64 ~/Desktop/test (test)
$ git cat-file -p 000fba
100644 blob 07cf0edd220a022c7447a8cf111bac3ed3422db9 a
100644 blob af379ed27a46022aca5a851bc55a8f69fd7afe3a b

.git/objects/07/cf0edd220a022c7447a8cf111bac3ed3422db9 blob对象
this is file a
this is master branch
this is test branch (file a)

.git/objects/af/379ed27a46022aca5a851bc55a8f69fd7afe3a blob
$ git cat-file -p af379e
this is file b
this is branch master(b)
  1. git reflog 罗列出所有的commit对象的sha-1码和commit提交内容,看到我们上面merge操作我们想要换药到之前的操作就用git reset –hard HEAD@{5}这里的HEAD@{5}是根据reflog查出来的,hard就是将暂存区内容还原到之前。

四、结束语

本文的分析全是有本人自己分析,分析的时间有点长,结果有点有仓促,如果有哪里写的不清楚或者不详细的请指正,小丁再次谢过。

文章目录
  1. 1. 一、git对象文件创建
  2. 2. 二、重写历史记录
  3. 3. 三、指令详解
    1. 3.1. 花絮
  4. 4. 四、结束语