5

一、传送

彻底掌握git(一)
彻底掌握git(二)

二、分支基础操作

分支其实就是一个指针指向某个commit提交,每进行一次提交,指针都会移到最新提交的位置。类似于串珠子,每次提交就像是一个一个的珠子,通过分支这个指针串联起来。如下图所示
分支.jpeg

执行git init命令后,git会给项目自动创建一个空白的主分支,即master分支
① 查看分支
可以通过git branch命令查看当前项目中存在哪些分支,但是该命令只能查看本地分支,无法查看远程分支,如果要查看远程分支,那么我们需要带上-a参数,即git branch -a

> git branch -a
* master
 remotes/origin/HEAD -> origin/master
 remotes/origin/branch-a
 remotes/origin/maste

带星号的那个分支表示是当前活跃的分支,远程分支以remotes/origin/开头,之后的为远程版本库的分支名

② 创建分支
分支必须要至少一次提交,没有提交的分支是不能通过git branch -a命令查看到的。我们可以通过git branch <分支名>命令来创建分支,创建的分支会以当前分支为基础,也就是说,新创建的分支会带上当前分支的所有提交

// 当前在master分支,执行命令创建branch-1
> git branch branch-1
// 创建的branch-1分支拥有和master分支一样的提交信息

当然我们可以指定新创建分支的指针位置,即只要部分提交信息,可以通过git branch <分支名> <commit-id>,那么创建的分支的提交信息,将仅仅包含当前分支的第一个提交到<commit-id>的这部分提交信息

> git branch branch-2 c12ac1

默认情况下是以当前分支为基础创建新的分支,我们也可以指定以某条已经存在的分支为基础创建新分支,git branch <新分支名> <旧分支名>,如:

// 创建的branch-3将以branch-2分支为基础
> git branch branch-3 branch-2

③ 切换分支
git branch命令创建好分支后,并不会自动从当前分支切换到新创建的分支上,如果我们需要切换到其他分支,那么可以通过git checkout <分支名>命令切换到指定分支上,如:

// 切换到branch-a分支上
> git checkout branch-a 

当然checkout还可以做到创建并切换到新分支上,那就是加上-b参数,即git checkout -b <分支名>,如:

// 创建并切换到branch-4分支上
> git checkout -b branch-4

checkout命令还可以通过--orphan参数,创建一个孤儿分支,这是一个没有任何提交的分支,所以其无法被查看到,也叫空白分支,如:

// 创建一个没有任何提交信息的branch-5分支
> git checkout --orphan branch-5
Switched to a new branch 'branch-5'
> git branch
 branch-1
 branch-2
 branch-3
 branch-4
 master

可以看到新创建的branch-5孤儿分支并没有被查看到,只有进行提交之后,才能被查看到。
利用孤儿分支,我们可以清空某个分支的所有提交记录,如:

// 创建并切换到一个孤儿分支
> git checkout --orphan empty-branch
// 此时项目中所有的文件对于这个孤儿分支来说都是新文件,直接添加到暂存区中或者全部删除都可以
> git add .
// 进行一次初始化提交
> git commit -m 'init commit'
// 删除要清空提交记录的那个分支,比如清空master分支
> git branch -D master
// 将这个孤儿分支的名称修改为刚才删掉的那个分支即可
> git branch -m master
// 强制同步到远程服务器
> git push -f origin master

需要注意的是,我们切换分支的时候,如果我们修改的文件是刚创建的还没有提交过到版本库中,那么不管这个文件的修改是在工作区还是暂存区,都可以切换到任意分支,因为该文件还没有被版本库追踪;如果我们修改的文件已经提交过,即被版本库追踪了,那么不管这个文件的修改是在工作区还是暂存区,都是无法切换到其他分支的,不过我们可以通过git checkout --force <分支>的方式强制切换,但是这样会导致之前的修改被丢弃

④ 删除分支
删除分支,我们可以通过git branch -d <分支名>命令,需要注意的是,删除分支的时候是无法自己删除自己的,也就是说,无法删除当前活跃分支。当我们创建好了一个新的分支后,如果还没有在这个分支上进行过任何提交,那么我们可以直接删除,因为新创建的分支上没有提交过新的东西,版本库没有发生变化。但是如果我们在新创建的分支上提交过新的修改,那么新的分支上有了新的东西,那么这个时候删除分支就会提示,要删除的分支上有还没有被merge的东西,而无法删除,当然可以通过git branch -D <分支名>的方式进行强制删除。

// 当前在master分支上
// branch-1提交过新的修改
> git branch -d branch-1
error: The branch 'branch-1' is not fully merged.
If you are sure you want to delete it, run 'git branch -D branch-1'.
> git branch -D branch-1
Deleted branch branch-1 (was 091034a).

⑤ 恢复分支
由于删除分支的时候,只是删除了指向相关提交的指针,但是该提交对象依然会留在版本库中,所以我们只要找到删除分支时的散列值,那么就可以通过git branch <分支名> <删除分支的散列值>恢复删除的分支,例如,上面删除分支的时候的时候散列值为091034a,如:

// 恢复branch-1分支
> git branch branch-1 091034a

当然我们可以同git reflog找到删除分支的时候的散列值

三、合并分支

分支合并是非常常见的操作,比如,我们在主线分支上开发新功能,同时在另一个分支上进行bug的修复,我们要将bug的修复合并到主线上;除了两条分支上需要合并之外,同一条分支上也存在着合并的情况,比如,有两个开发者都在主线分支上进行开发,一个开发者A提交代码后,另一个开发者B需要把开发者A提交的代码同步过来,也需要将A的提交进行合并。我们可以通过merge命令并且指定一个分支名,可以将指定的分支合并到当前分支上

// 切换到master分支上
> git checkout master
// 将branch-1分支合并到master分支上
> git merge branch-1

git merge branch-1等价于git merge branch-1 master ,merge后面的第一个参数是源分支第二个参数为目标分支

屏幕快照 2020-01-03 下午3.12.40.png

从图上可以看到,branch-1由master分支的第二次提交处分叉而来,同时master分支上进行了第三次提交,而branch-1分支上则进行了第四和第五次提交,然后将branch-1上的提交合并到master分支上,在合并的过程中,额外产生了一次合并提交,即第六次提交。通过git log我们可以看到这六次提交都在,我们可以通过git log --graph查看到分支的图形化结构。

① 分支合并的过程中发生的事
进行合并的时候,git能够自动进行合并。如果两个修改的是同一个文件但是修改的地方不一样,也就是说,修改的不是同一行,同一个文件在两个不同的分支上显示不同,比如a分支修改的是第一行,b分支修改的是第二行,对于a分支来说,该文件的第二行内容与b文件的第二行内容不同,对于b分支来说,该文件第一行内容和a分支的第一行内容不同,那么这个两个分支合并的时候,该文件第一行和第二行都出现了差异,但是合并之后该文件只有一个版本了,那么git是如何确定最终合成应该怎么显示呢?git就是找到两个分支的分叉点以分叉点为参照,所以第一行显示a分支的修改,第二行显示b分支的修改。如图所示。
分支合并2.png

从图中可以看出,如果不以分叉点为参照,那么合并的时候,第一行就不知道要显示分支a修改了第一行还是原始内容,同样第二行也不知道要显示分支b修改了第二行还是原始内容

② 冲突
在实际的合并中,可能会出现两个开发者同时对同一个文件的同一行代码进行修改的情况,那么这个时候git就会出现合并冲突,因为这两个修改相对于分叉点而言都是最新的修改,并且都是同一行,所以git不知道此时应该用哪个修改才正确,这个时候我们就需要手动解决冲突。

// 将branch-1分支上的提交合并到master分支上
> git merge branch-1 master
Auto-merging foo.txt
CONFLICT (content): Merge conflict in foo.txt
Automatic merge failed; fix conflicts and then commit the result.
> git diff foo.txt
**diff --cc foo.txt**
**index 5f5fbe7,da8092b..0000000**
**\--- a/foo.txt**
**+++ b/foo.txt**
@@@ -1,3 -1,3 +1,7 @@@
 1
 2
-3
-4
++<<<<<<< HEAD
++3
++=======
++4
++>>>>>>> branch-1
> git add .
> git commit -m 'merge branch-1 to master'

合并过程中出现了冲突,我们找到出现冲突的那个文件进行手动修改,需要去除冲突标志,然后将其添加到暂存区并提交即可完成合并
当然修改好冲突文件并添加到暂存区后,我们还可以通过git merge --continue命令继续merge操作,会自动弹出提交交互窗口,输入提交信息退出编辑即可完成合并。

需要注意的是,merge操作的过程中,不管两个分支合并后是否存在冲突,都会额外产生一次提交,只不过,如果合并后没有冲突,那么merge操作不会被中断,会自动添加一次合并的提交;如果合并后有冲突,那么merge操作会被中断,需要解决冲突后,将其添加到暂存区中,然后通过git commit 或者 git merge --continue继续merge操作

③ 快速合并
正常情况下进行合并操作,不管能不能自动合并成功,都会产生一次用于记录合并操作的提交信息,以便我们对版本库的发展过程进行追踪,但是当我们从某个点分叉出一个分支后,一直都没有在这个分支上提交过,那么我们在这个分支上进行合并的时候就变得非常简单,只需要把该分支的指针移到一下即可,而不需要产生一次合并提交了,我们称这种提交为快速合并提交。快速合并提交不利于版本库的追踪,所以我们可以通过--on-ff来强制快速合并产生一次合并提交,如:

> git merge --on-ff master branch-1

四、rebase变基净化提交历史

由于merge命令合并后会额外产生一次关于合并的提交,这样会导致我们的分支不断地的分叉又不断的合并,分支结构将会变得非常复杂,rebase命令的作用与merge相同,都是合并某个分支的内容到当前分支(目标分支)上,但是rebase不会额外产生一次提交,而是以要合并到目标分支的那个分支(基分支)为基,然后重播目标分支上的差异提交,也就是说rebase会将目标分支上的提交放到分支的最顶部
变基的原理很简单,git会让我们想要移动的提交序列在目标分支上按照相同的顺序重现一遍,相当于我们为各个原提交做了一个副本,它们拥有相同的修改集、同一作者、日期、以及注释信息。
① 平滑合并分支
git rebase <源分支> <目标分支>类似于merge命令,作用就是将源分支的内容合并到目标分支上,只不过不会额外产生一次合并提交,源分支其实就是基分支,即以源分支为基重播的是目标分支上新的提交

> git checkout master
// 将bug分支合并到master分支上
> git rebase bug master

需要注意的是,最终结果虽然是将bug分支相对于master分支上新的修改合并到master分支,但是变基的时候,是以bug分支为基的,即bug分支上的提交记录不变,然后重播master分支上的提交记录,所以master分支上的提交id会发生变化. 这个时候bug分支没有变化,所以其分支指针就在基点位置,这个时候bug分支相对于变基后的master分支而言,没有进行任何提交,相当于从基点创建了一个bug分支,这个时候只需要在bug分支上对master分支进行快速合并即可让bug分支的指针跳到master分支指针处。

或者说是,首先将master分支上相对于bug和master分叉点位置的提交取消,并且把它们临时保存为补丁放到.git/base目录中,然后把master分支的head更新为bug分支head处,最后把刚刚保存的补丁应用到master分支上

// 切换到bug分支
> git checkout bug
// 快速合并master分支内容
> git merge master
// 此时bug分支指针和master变得一致

当rebase的时候,如果出现了冲突:
git rebase --abort 即放弃合并,会回到rebase之前的状态,之前的提交不会丢弃;
git rebase --skip 会跳过目标分支中导致冲突的提交,即保留基分支中的提交,移除当前目标分支中会导致冲突的提交,自动解决冲突,rebase之后目标分支上将看不到会导致冲突的提交了,慎用
git rebase --continue 需要配合git add使用,即解决冲突后,将最终的修改添加到暂存区中,然后通过--continue继续rebase操作,即手动解决冲突。

② 拷贝提交到某个分支上
git rebase startPoint endPoint --onto <目标分支>
首先切换到拷贝的源分支上,找到要拷贝的提交区间,然后通过--onto指定目标分支,执行命令后会产生一个游离分支,这个游离分支和我们要的最终结果是一样的,但是还没有拷贝到目标分支上,然后切换到目标分支上,此时会提示目标分支遗留了一段提交,即我们拷贝的那段提交,然后会有游离分支的head位置,我们在目标分支上将分支指针重置到游离分支的head即可

// 切换到master分支
> git checkout master
// 将master分支上(5af873,89da14]的提交复制到bug分支上
> git rebase 5af873 89da14 --onto bug
// 查看产生的游离分支
> git branch
* (HEAD detached from 838de4f)
 bug
 master
// 切换到目标分支
> git checkout bug
Warning: you are leaving 2 commits behind, not connected to
any of your branches:
 7a48d4c commit-4
 78c3b66 commit-3
If you want to keep them by creating a new branch, this may be a good time
to do so with:
git branch <new-branch-name> 7a48d4c
// 可以看到git提示目前分支遗留了从master上拷贝的两个提交,并且头部在7a48d4c,然后重置目标分支指针到7a48d4c即可
> git reset --hard 7a48d4c

当然,复制某个或某些提交到某个分支上,我们可以更加灵活的cherry-pick命令,我们只需要找到需要复制的commit-id,然后切换到要复制的目标分支上,执行git cherry-pick <commit-id1> <commit-id2> ...命令即可

// 切换到master分支上
> git checkout master
// 将ee4dd5这个提交复制到master分支上
> git cherry-pick ee4dd5
// 查看ee4dd5对应的提交有没有复制成功
> git log

③ 合并多个commit为一个完整commit
rebase 提供了一个-i参数,可以让我们进入交互操作界面,git rebase -i startPoint endPoint找到要合并为一个提交的区间段(startPoint,endPoint],然后会进入一个交互操作界面,如:

> git rebase -i 5af873 89da14
pick 82a82f3 commit-3
pick 89da145 commit-4
// 现在我们可以将这两个提交合并为一个,比如将commit-4合并到commit-3中,那么我们可以将commit-4前面的pick改成s,s表示合并提交
pick 82a82f3 commit-3
s 89da145 commit-4
> :wq退出编辑模式
// 然后会再弹出一个交互界面用于修改提交合并后的信息,默认有可以不改
> :wq退出编辑模式
// 查看提交历史可以看到两个提交合并成了一个
> git log

【Git】rebase 用法小结
④ 修改很久之前的某一次提交
对于最近的一次提交,我们可以通过git commit --amend命令进行修改,那么很久之前的修改我们可以通过rebase命令进行修改,git rebase -i startPoint

// 因为起点取不到,所以找到要修改的哪次提交的上一次提交作为起点
> git rebase -i 5af873
pick f42d811 commit-3
pick 07fe470 commit-5
// 将要修改的那次提交前的pick修改为e,表示要编辑
e f42d811 commit-3
pick 07fe470 commit-5
> :wq退出编辑模式
// 进入修改模式
> git commit --amend
// 修改好之后保存
> :wq退出
// 此时查看log指针只在修改的位置,因为rebase还没结束,需要继续
> git rebase --continue
// 再次查看log,可以看到之前很久的一次提交被修改了

⑤ 多分支变基
假如有三个分支,我们可以把某个分支相对于某个分支的变化应用到某个基分支上,git rebase --onto <基分支> <相对分支> <目标分支>,即以基分支为基础,然后找到目标分支相对于相对分支的变化并复制一份变化,然后将其运用到基分支上,即在基分支上进行重播一遍,然后将目标分支指针移到重播后的位置上,其执行过程为,先checkout切换到基分支,然后应用变化,在checkout切换回目标分支,将指针移到变化后的位置,如:
18333fig0331-tn.png
如果想要将C8和C9先应用到master分支上,那么我们可以通过git rebase --onto master server client命令以master分支为基础,找到client相对于server共同分叉点的变化即C8、C9,然后将其应用到master分支上,即基分支与变化连接在一起,然后将client分支指针移到连接后的位置,即
18333fig0332-tn.png
Git分支Rebase详解

⑥ 删除分支上的某个或某段提交
如果在提交的过程中发现前面的某个提交有错误,那么我们可以直接删除,git rebase --onto startPoint endPoint <要删除提交的分支> 这里的startPoint是取不到,但是endPoint是可以取到的,所以是一个前开后闭的区间,即以当前分支的startPoint处为基,以当前分支相对于endPoint之后的提交为变化,应用到当前分支,即从startPoint处开始播放endPoint之后的提交,所以相当于是删除了startPoint到endPoint之间的提交,如:

// 删除master分支上(5af873,5305f6]之间的提交
> git rebase --onto 5af873 5305f6 master

在rebase的过程中,如果出现冲突,那么rebase操作也会被终止,冲突解决完成后,需要将其添加到暂存区中,但是此时无需进行git commit操作,因为rebase操作不会产生新的提交,而是直接通过git rebase --continue 继续rebase操作或者通过git rebase --abort取消rebase操作

五、rebase变基的实际应用

① 多人协同开发的时候避免出现钻石链
在实际开发中,并不是所有人都会用rebase净化提交历史的,所以会经常出现钻石链的情况,假如有一个开发进行了一次提交并推送到了云端;然后另一个开发者也进行了一次修改,但是二者修改的不是同一个文件,然后其通过git pull将上一个开发者提交的内容拉下来,由于git pull会进行merge操作,会自动创建一个合并提交,所以第二个开发者将得到一个钻石链,然后将其推送到云端;然后另外一个开发者也进行了一次修改,他修改的也不是同一个文件,即使他使用的是git pull --rebase命令,那么前面两个开发者产生的钻石链也还在并没有消失,这个时候怎么办呢?首先用git log --graph找到分叉点的提交id,然后使用git rebase -i <分叉的位置提交id>表示对分叉点之后的提交进行变基操作,之后会弹出交互窗口,由于三个人的提交修改的都不是同一个文件,那么此时不用进行任何操作,直接退出交互窗口的编辑即可净化提交历史。

// 第三个开发者得到了第一个和第二个开发者合并产生的钻石链
> git log --graph
// 找到分叉点提交id
> git rebase -i 5af873
// 打开交互命令窗口,直接退出不用进行任何操作
> :wq 退出编辑模式
// 再次查看提交历史是否已经被净化
> git log --graph

如果三个开发者修改的都是同一个文件,由于变基操作会重播一遍,所以这三个提交都会在当前分支上重播一遍,并且是一个一个轮播的,由于三个人修改的都是同一个文件,所以会有两次合并冲突,每解决一次冲突,将改好的文件加入到暂存区中,然后执行git rebase --continue,解决完两次冲突后再执行git log --graph查看提交历史是否已经被净化了。

备注: 当然也可以直接在当前分支上执行git rebase命令,不带任何参数,会自动在当前分支上进行变基操作,对于简单的可以直接用git rebase命令净化即可,如果git rebase命令命令无法完全净化,那么就需要利用上面的方式一步一步进行rebase了。


JS_Even_JS
2.6k 声望3.7k 粉丝

前端工程师