« 主页

Git - Play With Commits

发布于

版权声明:眯眼探云原创,可随意转载,请保留该版权声明及链接:https://tyun.fun/post/23.git-play-with-commits/

深入理解 gitrevisions里面我分享了自己对 git-revisions 的理解,理解 git-revisions 最终的目的是为了让我们更好的使用 git,也就是:

Play with Commits

Git Objects

前面我们提到其实 git-revisions 归根结底,就是 commit,那每个 commit 中具体包含了什么东西呢?

官方文档中有详细的介绍,使用 $git cat-file -p <commit-id> 可以查看每个 commit 中包含的内容,例如:

$git cat-file -p 7bb0eb150e44547c578fe2b2a0d75c53add968c3
tree baec93cb3611913684f3c751977879591b6a424a
parent 5aa2cf55499447391963ce7fada70e7dd2fc6c5c
author Edward Gu <edhroyal@gmail.com> 1504074234 +0800
committer Edward Gu <edhroyal@gmail.com> 1504074234 +0800

Fix buy apple

这其中包含了 tree,parent,author(和日期),committer(和日期),commit message 这些元素。

然后通过某种算法,由这些元素最终算出了这个 commit-id:7bb0eb150e44547c578fe2b2a0d75c53add968c3。

parent 代表的是上一个 commit-id。(如果该 commit 是一个 merge,那么可能会有多个 parent 属性。)

author 代表该 commit 的作者,或者讲精确一点:创建者。

committer 代表最后一个做出 commit 动作的人。

commit message 即当前 commit 的描述信息 Fix buy apple

重点讲一下 tree

git 中的每个文件(的特定版本),不论类型,都是以一个 blob 对象来保存。

blob,水滴。所以每个项目都是由水滴汇聚而成的大海,非常形象。

而 tree 就是一个虚拟的文件夹,我们可以继续用 $git cat-file -p <tree object id> 来查看某个 tree 中包含的内容,比如刚才示例中的 tree:

$git cat-file -p baec93cb3611913684f3c751977879591b6a424a  
100644 blob 51d13237d33e1a1348f8ababe3371b1e68d790bf	README.md  
100644 blob 5caecdc43a0e6eb3d1a672c5faf685426222717b	Something  
100644 blob fd04bfbd5d1d4faebdc2c0699414d74b66117d6a	buy

注意了,这个 tree 包含的是整个项目的当前状态哟!

纳尼!整个项目的当前状态?

这里就必须说明一下 git 保存文件的策略,怎么做到这一点。

第一,git 每次保存的不是文件的 diff,而是文件的当前版本。

比如 A 文件在当前 commit 中被修改过,我们可以认为被改过后的文件是 A’,那么这个 commit 中保存的就是文件 A’,而不是 A’ - A。

第二,每个文件都通过算法来得到一个唯一的 id,然后每个文件夹中的所有文件 id 一起算出文件夹(对应 tree)的 id。

经过这么处理,每个 commit 中只需要保存根目录的 tree 对应的 id,就可以很方便的确定项目的当前状态了。

请多用 git cat-file -p <commit-id/tree-id> 来查看一下里面的具体内容,并不难理解。

如果每个 commit 中的 tree 代表的是项目的完整状态,那么我们如何知道当前 commit 具体改了什么东西呢?

回顾一下

现在我们已经理解了一个 commit 中包含的关键元素,那么接下来要玩转 commit,就没有那么难了。

我们再来回顾一下一个 commit 中所包含的内容

tree - 项目的当前状态
parent - 前一个 commit id
author - 作者,创建者
committer - 最后做出 commit 动作的人
commit message - commit 的描述信息

【问题】git 中的 blob 文件中包含的是文件的当前版本,还是它与上个版本的 diff?

Git - Checkout

checkout 翻译为检出,也就是从代码仓库中把某些东西拿出来,我们可以检出整个 commit 的状态,也可以检出某个 commit 中的部分内容。

还记得之前提到的不?branch、HEAD、tag、refs……等等这些,全都是指向的 commit 哟~

也就是说,这些统统都是 checkout 检出的对象。

不过在实际的使用中,还是有一些细微的区别。

当我们 checkout 一个 branch 的时候,我们其实是切换到那个 branch 上面去。

$git checkout branch_A

当我们 checkout 某个特定的 commit 的时候,因为没有分支信息,所以 git 会提示你,你在一个 detached HEAD。

当然你可以在 checkout commit 的时候,同时指定一个分支名字,这样就可以在 checkout 的时候同时以目标 commit 为状态创建一个新的分支。

$git checkout 7bb0eb150e44547c578fe2b2a0d75c53add968c3 -b branch_B

Checkout 某个 commit 中的特定文件

这个是一个非常有用的功能。

最基本的使用形式,就是 $git checkout . 来回退当前的改动。

“当前的改动”指的是,还没有被添加到索引(或者叫 staging area)的改动,也就是,还没有执行 $git add xxx

另一种最常见的用法就是从获取特定文件在特定 commit 中的状态。

$git checkout 7bb0eb150e44547c578fe2b2a0d75c53add968c3 somefile

比如我需要将某个文件回退到特定的版本,那么我直接将相应的文件从某个 commit 中 checkout 即可。

【问题】如何用 checkout 来切换分支?
【问题】如何检出某个文件的特定版本?

Git - Cherry-pick

当我们需要在不同的 branch 之间移动某个 commit 的内容的时候,我们怎么做呢?

Cherry-pick 摘樱桃。处理代码的改动是需要非常小心的,就像摘樱桃一样,也需要非常小心。

这个命令可能对许多同学来讲比较陌生,设想这么一种情况:

branch_A: ... - commit A - commit B
branch_B: ... - commit C - commit D

现在我发现,branch_B 需要 commit B,那么我就要把 commit B 拿到 branch_B

这种时候我们需要的命令就是 cherry-pick。

$git cat-file -p 7bb0eb150e44547c578fe2b2a0d75c53add968c3
tree baec93cb3611913684f3c751977879591b6a424a
parent 5aa2cf55499447391963ce7fada70e7dd2fc6c5c
author Edward Gu <edhroyal@gmail.com> 1504074234 +0800
committer Edward Gu <edhroyal@gmail.com> 1504074234 +0800

Fix buy apple

我们还是看前面的例子,如果上面的内容是 commit B,那么我们进行 cherry-pick 的时候,到底些内容会被 pick 到当前的分支呢?

parent 根据刚才的情况来看,parent 显然会发生改变。(实际情况中,也可以在 parent 相同的情况下进行 cherry-pick,但通常意义不大。)

author 是创建者,所以会被保留。

committer 这一项一定会发生变化,因为即使是同一个人,那么时间戳是不一样的。

commit message 默认会被保留。

还是要重点讲一下 tree

最终要的部分,还是 tree。

cherry-pick 拿到新 commit 中的就是整个 tree 吗?

不是的。

真正被 cherry-pick 的,是这个 tree 和他的 parent 的 diff。

其实也很好理解,因为我们本来就是要把 diff 拿过去。

【问题】当进行 cherry-pick 的时候,目标 commit 的那些内容会被拿到当前分支上?

Git - Rebase

讲完 cherry-pick 再来讲 rebase,其实就比较好讲了。

rebase 的最终干的事,就是替换 parent。

所以有的翻译叫“变基”,虽然听起来比较傻,但也的确反映出了 rebase 实际干的事情。

虽然 rebase 可以处理多个 commit 的情况,但这里只讨论当前分支只有一个新 commit 的时候进行 rebase。

那现在我们来看这两个分支:

branch_A: ... - commit A - commit B
branch_B: ... - commit A - commit C  

那么如果我们在 branch_A 上面执行 $git rabase branch_B 的时候,发生了什么呢?

第一步,我们把 branch_A 同步到 branch_B 的最新状态,也就是说,branch_A 已经和 branch_B完全一样了,也是 ... - commit A - Commit C

第二步,我们把 commit B 放到 branch_A 的最新位置。怎么放呢?记得前面提到的 cherry-pick 吗?完全一样的哟!

最终版本看起来就是这样的:
branch_A... - commit A - commit C - commit B'
(B’ 指 rebase 过后的 commit B)

:这里是为了方便理解这么来进行描述的,实际的处理过程不一定是这样。

【问题】rebase 的核心是替换了 commit 中的哪条属性?

Git - Merge

直接将两个(多个)分支的最新状态依次合并起来。

这应该是我们用的最多的命令了,只讲一下 fast-forward 与 none fast-forward 的区别。

branch_A: ... - commit A
branch_B: ... - commit A - commit B - commit C

那么当在 branch_A 分支执行 $git merge branch_B 的时候,就是一次 fast-forward merge。

这种情况下,branch_A 只是比 branch_B 落后几个 commit。

fast-forward merge 操作其实只是更新了 branch_A 指向的位置,将其指向了 commit C。

除开 fast-forward 就是 none fast-forward 了。

none fast-forward merge 的特点就是,一定会生成一个 merge 的 commit。

有时根据情况的需要,我们也可以在可以 fast-forwad 的情况强行生成一个 merge 的 commit,只需要在命令中带上 --no-ff 的参数就可以了。

比如我们 gitflow 流程中就专门提到了 --no-ff 的使用。

冲突的处理

cherry-pick,rebase,merge 这几条命令都会可能产生冲突。

很多人觉得解决冲突非常讨厌,但在团队协作中,这是必不可少的一部分。而且解决冲突其实并不困难,git 会将冲突标记出来,而你要做的:

  • 一、了解冲突最终需要保留哪些内容
  • 二、挨个把那些不需要保留的部分干掉
  • 三、验证一下最终的结果是否OK

一个小小的技巧就是,每改动完一个冲突,都保存一下状态,对相应的文件执行 $git add <file name>

当冲突处理失败的时候,可以使用 abort 操作来进行放弃本次操作(cherry-pick/rebase/merge)。

当冲突处理成功以后,使用 continue 来完成这次操作(cherry-pick/rebase/merge)。

比如:

// resovle conflicts  
$git add .    // add result in to staging area  
$git cherry-pick/rebase/merge --continue  

当你不清楚当前状态的时候,一定记得多用 $git status 来进行查看。

记得多练习哦~

当你可以 play with commits 的时候,你才会真正的了解 git,你才会知道,单纯的像 svn 那样去使用 git,是多么可耻的一件事。

enjoy~