版权声明:本人文章仅在掘金平台发布,请勿抄袭搬运,转载请注明作者及原文链接 🦉
阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉
网上稍微搜一搜,每一篇 git 文章都在教你怎么 git add,git push,少数文章会给你说 git cherry-pick,git stash,但是很少有文章会给你说 git rebase。
从刚工作时起,你的 leader 前辈们都在告诉你,git rebase 很危险,平时就用 git merge 就行了,所以你工作了三五年,还没怎么用过 git rebase,有时候遇到 git 问题,不是删分支就是在删分支的路上,我猜你还会把需求代码先一点一点复制粘贴下来,然后删除旧分支再建新分支,再改回去,对吧?
stop!!!
诶棒油,你的头顶长了什么东西,我眼睛里没有~
那么今天,我们 git rebase 要讲,git cherry-pick、git stash 也要讲,而且要讲一些可能大家不知道的,实用的 git 知识和操作。
带着问题找答案,先来看看下面八个问题,你是否能解决。如果你都能解决,那么恭喜你,你的 git 已然化境:
(前四个问题只能算是开胃小菜)
- 新需求忘记开新分支,提交到其它分支了,怎么办?
- 新开发了一个功能,合并 master 了,产品过了一段时间说不要了,怎么办?(不可以殴打产品)
- 功能开发了一半,需要拉一下 master,但是你又不想 commit 代码怎么办?
- 刚才提交了一个 commit,发现 message 写得有问题,怎么办?
- 需求开发提交了几个 commit,提交 master 时领导 review 后,说你第一笔 commit 代码有问题,让你改一下,怎么办?
- 刚才提交了很多个 commit,发现最初的 commit meesage 写得有问题,怎么办?
- 一个大需求开发了一个月,每天拉 master 代码合并到本地分支,发现这个需求自己提了十多个 commit,需求需要发布 merge 到 master 了,领导让你把十多个 commit 合并成一个,方便 code review,怎么办?
- 昨天提交了一个 commit 到 master 了,今天发现 commit message 写错了,但是 master 上别人的提交已经有几十上百个了,领导说你 message 写错了,改一下,怎么办?
不能完全解答的朋友就请继续往下看。
为了方便演示,我现在新建一个 git-demo 的项目,新增 a.js、b.js、c.js、d.js 四个演示文件,每个文件一句代码,每个文件一次 commit 共四个 commit:
问题1:新需求忘记开新分支,提交到其它分支了,怎么办?
我当初犯过最严重的错误就是,同时开发两个需求,忙来忙去,两个需求的 commit 搞岔了,两个分支都各有另一个需求的 commit,当时带我的同事都懵了。
然后在他一顿猛如虎的操作下,才理清了我的 commit。
其实答案很简单,就是 cherry-pick,这倒不是什么新鲜知识,很多有点工作经验的都知道,那为什么我们还讲,一是照顾新同学,二是这个命令对我们后续的题目解答很重要。
git cherry-pcik 的作用,就是将任意分支的 commit 挑拣到当前分支,但是这个挑拣不会删除原分支的 commit。
使用方式,就是紧跟着我们某个 commit 的 hash 值,可以跟多个,每个 hash 使用空格分割:
bash
git cherry-pick 123abc 456def 789ghi
不妨在演示项目中新建一个分支 test:
我们在 test 分支新增一个 commit:
然后我们复制 e.js 的 hash,切回 master 使用 cherry-pick 将其应用到 master 上:
而 test 分支的 e.js commit 不会受到影响:
这就是 cherry-pick 最简单的应用。
不过,你聪明的小脑袋瓜可能会问,cherry-pick 任意分支?那挑拣的 commit 如果是在本分支呢?
诶,问得好,我们不妨试试。
先将刚才的 e.js 从 master 撤回。然后将 e.js、d.js 做 cherry-pick,这里的 e.js 是 test 分支的,d.js 是 master 分支自己的,我们在 master 分支做 cherry-pick 操作:
注意看红框里的内容,第一个框里说的意思,就是上一个 cherry-pick 是空的,可能是解决冲突产生的,你可以直接执行 git commit --allow-empty
将没有任何内容更改的 commit 提交到当前分支。
假设我们直接执行 git commit --allow-empty
,会弹出 vim 编辑器让我们编辑该 commit 的信息:
我们不做任何操作,直接按 esc
键,然后输入 :wq
并按回车键退出:
可以看到,d.js 有两次 commit:
回到前面,除了 git commit --allow-empty
,git 还提示我们有其它几个命令可以选择:
- git cherry-pick --skip
- git cherry-pick --continue
- git cherry-pick --abort
git cherry-pick --skip
的意思就是跳过,d.js 本身对比 master 自身来说没有任何修改,是空的,直接跳过,仅 cherry-pick e.js。
git cherry-pick --continue
的意思是说,当你使用 git cherry-pick 遇到冲突,解决冲突并把修改添加到暂存区(使用 git add)之后,就可以使用这个命令,让 git 继续执行 cherry-pick 操作。如果你没解决冲突,一直执行这个命令,是不会有任何有意义的效果的。
git cherry-pick --abort
则是放弃此次 cherry-pick,只要放弃了,所有内容都不会 pick,包括 e.js 也不会被 pick 过去,等于直接放弃本次 pick 操作。
综上,如果你在 cherry-pick 时不小心 pick 了本分支的 commit,且是空白内容没有实质性内容冲突,最好执行 git cherry-pick --skip
。但是实际上,那么多个 commit,它只会提示你 The previous cherry-pick is now empty, possibly due to conflict resolution.
,你并不知道具体是哪个,直接跳过也不见得是正确的,也可以先 pick 过来,再决定取舍。
另外,cherry-pick 的 commit 理想状态下是没有冲突的,但是很多时候会有冲突,必须解决冲突了才能继续 cherry-pick。
具体点说,冲突解决完,需要继续 git add 文件1 文件2 ...
,然后修改 commit message。
这两步做完,还需要继续执行:
bash
git cherry-pick --continue
这样呢,一次 cherry-pick 的过程才算结束。
再多说一句,git cherry-pick 本就应该用于挑拣其它分支的 commit,所以用的时候不要挑拣本分支 commit。
问题2:新开发了一个功能,合并 master 了,产品过了一段时间说不要了,怎么办?
这个问题也不算太难,直接 git revert。
使用方式,也是跟 hash 值:
bash
git revert 123abc 456def
当然,上面的用法是针对不连续的 commit 来说的,如果你是连续的多个 commit 一起撤回,可以这么用:
git revert <start_commit>(不包含)..<end_commit>(包含)
,例如:
bash
git revert abcdef123..7890abcd
问题3:功能开发了一半,需要拉一下 master,但是你又不想 commit 代码怎么办?
git stash
。
又是一个高频使用的命令,重要性不言而喻。场景很多,比如,我在当前 bug_fix 分支,在修复一个 bug,突然来了一个优先级更高的 bug,那已经写的代码不能直接 commit 吧?你当然可以说建一个新分支呗,一个 bug 一个分支,这也是一个解决方案,不过各家公司有各家公司的要求,具体问题具体分析。不切换分支的情况下,就可以 git stash。
修复完这个紧急 bug 后,我们需要继续修复前一个 bug,就可以执行 git stash apply
将之前暂存的代码恢复,继续开发。
不过,这里需要注意,git stash apply
是应用最近一次 stash 的代码,如果你存了很多个,就必须指定。
我们可以通过 git stash list
命令查看所有的 stash:
bash
stash@{0}: On main: stash1
stash@{1}: On feature: stash2
stash@{2}: On feature: stash3
stash@{3}: On main: stash4
越上面的越新,如果我们要应用某个旧的,指定一下即可:
bash
git stash apply stash@{3}
git stash 和 git commit 一样也可以设置 message:
bash
git stash -m 'stash: 这是一个 stash'
篇幅所限,我们用表格汇总下 stash 命令:
命令 | 作用 | 示例 |
---|---|---|
git stash 或 git stash push |
将当前工作目录和暂存区的修改保存到栈中 | git stash push -m "保存修改" |
git stash list |
查看当前保存的所有 stash | git stash list |
git stash apply |
应用最近一次保存的 stash,应用后 stash 仍保留在栈中 | git stash apply |
git stash apply <stash编号> |
应用指定的 stash | git stash apply stash@{1} |
git stash pop |
应用最近一次保存的 stash,并将其从栈中删除 | git stash pop |
git stash pop <stash编号> |
应用指定的 stash 并将其从栈中删除 | git stash pop stash@{1} |
git stash drop |
删除最近一次保存的 stash | git stash drop |
git stash drop <stash编号> |
删除指定的 stash | git stash drop stash@{1} |
git stash clear |
删除栈中所有的 stash | git stash clear |
git stash show |
查看最近一次 stash 的差异 | git stash show |
git stash show <stash编号> |
查看指定 stash 的差异 | git stash show stash@{1} |
git stash show -p <stash编号> |
查看指定 stash 的详细差异内容 | git stash show -p stash@{1} |
问题4:刚才提交了一个 commit,发现 message 写得有问题,怎么办?
git commit --amend
。
当我们执行完命令后,会打开 vim 编辑器:
vim 编辑器的使用其实很简单,我们输入 i,底部会提示我们进入编辑模式:
我们使用箭头移动光标位置,输入新的 message:
修改完成,按 esc
键,再输入 :wq
回车,操作完成。
问题5:需求开发提交了几个 commit,提交 master 领导 review 后,说你第一笔 commit 代码有问题,让你改一下,怎么办?
这里的意思很简单,当我们辛苦开发了一阵子需求,提了好几个 commit 后,发现某一笔(非最新一笔)的代码有问题,需要修改,一般人的做法,就是库库一阵改了,再提个新的 commit 呗。
这其实也可行,但是还是那句话,不同的公司有不同的规范,commit 的管理尺度各不相同,所以不用太较真场景和问题的解决方法。
假设我们现在不新增 commit,就在原 commit 的基础上修改,这就需要用到 rebase。我们不讲深奥的理论,就只讲实操和现象,你先用起来再说。
现在,我们将 b.js 的 const a = 1
的内容修改一下,且不新增 commit。
我们需要执行命令git rebase -i <hash>
。
这里有个关键点,rebase -i 操作,后面跟的这个 hash 是一个开区间,也就是不包含在内的意思,假如,我要修改 b.js,hash 就是 a.js 的 hash,假如我要修改 c.js,hash 就至少得是 b.js 的 hash。
Talk is less,show me the code.
直接看结果,假设是 a.js 的 hash:
bash
git rebase -i 081b0c26a4d7deb04ed1625b2a84f31f24d5fbe8
那么我们就可以编辑 b、c、d 三个提交。
假设是 b.js 的 hash:
bash
git rebase -i b8cd836ef539457228a86fca8ddeb3ec2b52017e
那么我们就可以编辑 c、d 两个提交。
其实就是在这个开区间的 hash 范围内,所有的 commit 都可以被操作。
现在,我们就修改一下 b.js 的内容,不新增 commit:
这里绿色区域的文字,默认进入是 pick,当我们要操作某个 commit 时,将其替换为对应的操作,例如:
edit 的意思就是编辑该 commit。务必记得,在 vim 编辑器里,需要先键入 i
才能进入编辑模式。然后我们继续 esc
+ :wq
退出:
到了这一步,我们相当于穿越时空,来到了 b.js 提交的那个时空,我们在当前可以任意修改文件,任意操作代码,然后执行 commit,或者使用 git commit --amend
修改当前 b.js 的 message。
我们将 b.js 代码内容修改一下:
修改完代码,务必将修改后的代码暂存:
暂存完,这一步我们需要修改 commit message,如果我们不修改 commit message,我们直接 git rebase --continue
,系统还是会弹出 vim 编辑器让我们修改,不修改的话我们直接 esc
+ :wq
退出:
仔细观察红框中的内容,首先是 commit 没有什么变化,其次,master 分支名旁边的表示 rebase 进程的提示没有了。
那么,我们针对历史 commit 代码的修改就结束了。
好了,这时候聪明的你又会问,除了 edit,还有其它的操作吗?
有的,有的兄弟,这样的命令一共有 11 种:
pick 就是默认的,reword 是只修改 commit message 不修改代码,squash 是将多个 commit 合并为一个。
squash 这里比较重要,我们继续来举个例子,假设我一共 4 个 commit,那么到底什么场景下我会需要把它合并成一个呢?
首先,开发时间长,每天都可能要处理不同需求和 bug,有时就只能先 commit,然后去做其它事情。这样就会产生多个 commit,当我们开发完了,我们需要提代码给 leader review,你一个需求不能整个五六个、七八个 commit 给他看吧,这时候就需要合并 commit。
其次,假设我现在修改 bug,先写了一版,推上去发现有问题,我需要继续修改,此时我当然可以使用上面的 git rebase 的 edit 方法,不过你也可以再新增一个 commit,然后合并 commit,这也是一种方案。
OK,废话不多说。
假设我现在需要把 b.js、c.js 的 commit 合并为一个:
此时是 a.js 的 hash,才能选择 b.js 与 c.js。
bash
git rebase -i 081b0c26a4d7deb04ed1625b2a84f31f24d5fbe8
(s 是 squash 的缩写)
但是你发现,为什么第一个 s 是红色的?
其实是因为,在 squash 操作中,第一个 commit 不允许被 squash,第一个默认就是 pick。
假设合并 b & c,需要这样:
假设合并 c & d,需要这样:
假设合并 b & c & d,需要这样:
那么继续 esc
+ :wq
退出,如果有冲突会按照冲突流程处理,前面我们已经处理过。
可能看到这里,你又有疑问,如果 git rebase -i <hash>
的 hash 是开区间,那我就是要编辑或者合并 a.js 的 commit 怎么办?
可以使用参数 --root 解决:
bash
git rebase -i --root
必须这样执行,不需要加 hash。
这样又引出了新的问题,就是,我操作完了,发现搞错了,我怎么反悔?得回到 squash 前的状态啊!!
这里就涉及到 git 的 back 操作,想必你工作中也用到过,前面的内容中我们也提到了 revert。
那这里的回退,我们卖个关子,放在后面讲。
现在以表格整理一下这些命令,其余命令大家可以实操试试,毕竟实践出真知。
简称 | 英文全称 | 解释 |
---|---|---|
p |
pick |
按原样应用指定的提交,让提交按原有的顺序和内容应用到变基后的分支上。 |
r |
reword |
应用该提交,但会暂停以允许修改提交信息,可用于完善之前提交时写得不够清晰或有错误的提交信息。 |
e |
edit |
应用该提交,但会暂停以便修改提交内容,你能对此次提交所做的更改进行调整,之后使用 git commit --amend 更新提交。 |
s |
squash |
将该提交与前一个提交合并,并且可以编辑合并后的提交信息,有助于把多个相关的小提交合并成一个更有意义的大提交。 |
f |
fixup |
类似于 squash ,将该提交合并到前一个提交,但会丢弃当前提交的提交信息,只保留前一个提交的信息。 |
x |
exec |
在处理到该提交时执行一个 shell 命令,允许在变基过程中插入自定义操作,如运行测试脚本等。 |
d |
drop |
移除该提交,即不将此提交应用到变基后的分支中,可用于去除不必要的提交。 |
b |
break |
在该提交处暂停变基,让你可以手动检查状态或执行额外操作,之后使用 git rebase --continue 继续变基。 |
l |
label |
在当前位置创建一个新的标签,类似于 git label 命令,方便后续引用该位置。 |
t |
reset |
返回到指定的标签位置,撤销自该标签之后的所有变基操作。 |
m |
merge |
引入指定的标签或提交,将其与当前分支合并,就像执行了一次 git merge 操作。 |
问题6:刚才提交了很多个 commit,发现最初的 commit meesage 写得有问题,怎么办?
通过前面的学习案例,后续的问题我们直接给出参考答案,减少演示带来的阅读负担。
第一个你应该想到的思路,就是 git rebase -i,并执行 reword 或者 edit 命令。
第二个思路,就是直接 revert 该 commit,然后重新 commit 时修改 message。
问题7:一个大需求开发了一个月,每天拉 master 代码合并到本地分支,发现这个需求自己提了十多个 commit,需求需要发布 merge 到 master 了,领导让你把十多个 commit 合并成一个,方便 code review,怎么办?
git rebase -i,执行 squash 操作。
当然了,这是一个理想状态,你的需求的 commit 是连续的,只有你自己的 commit。
可现实开发中,一个大需求绵延一个月,我们每天还要更新 master 的代码,我们自己的 commit 可能散落在各个时间点上,不能直接 squash,不然你就会面对几百个上千个 commit,无异于大海捞针。
最正确的做法,是我先将开发完成的分支 push 到 remote 仓库,然后我新建一个 merge request,不要直接确认,我们只是需要其 diff 出分支上的更改。因为此时 gitlab 等托管平台已经会自动 diff 出本分支的 commit,这样你不用再去翻找一月前的那些 commit 了。
然后,你新建一个分支,将原来分支上所有的 commit (这就是为什么要在原分支新建 merge request,方便你复制 commit hash)使用 cherry-pick 转移到新分支上来。
接着使用 squash 操作将其合并为一个 commit,再推送新分支到 remote,发起真正的 merge request。
怎么样!!这个思路是不是很 sao ~。
问题8:昨天提交了一个 commit 到 master 了,今天发现 commit message 写错了,但是 master 上别人的提交已经有几十上百个了,领导说你 message 写错了,改一下,怎么办?
你可能会说,git rebase -i,执行 reword 或者 edit。
是的,这不算错。
但是这和第 6 题不同,这是已经在 master 上的修改了,不是本地的 commit。
如果我们执行 rebase 操作,因为涉及到变基,代码 push 到托管平台后,会 diff 出你的变更,它会把该 commit 后的所有 commit 都识别为你的变更,你会发现你这个分支提上去 merge 到 master 时会有 N 个 commit,那些 commit 都是你同事的,且已经是在 master 上了。
理论上你直接合并也没有问题,因为你本身没有修改过任何东西,哪怕合上去起了冲突,冲突解决完就行了。但是你的领导看着你的这个操作,明明只是修改了一个 commit message,但是却多出来 N 个已经在 master 的 commit,他看到肯定会懵逼,几乎不会同意你的 merge request。
所以,最好的解决方案是就是:
将错就错~?
哈哈,大概率现实是这样的,你的 leader 会和你说,错就错了,一个 commit message 而已。
这其实没毛病,但是如果管理严格一点的公司呢?
或者说,这个问题难道真的没法解决吗?
其实方案我们在第 6 题也提过。
解决办法就是先对其 revert,然后重新 commit,这是最稳妥的解决办法。
对,思路就是这么简单,不要拘泥于 git rebase 强大的历史 commit 能力,最简单朴素的方式或许是最安全的。
一点点后续:我想反悔,怎么办?
假如我们 git rebase 出错了怎么办?
假如你还没执行完,你的分支显示是这样的:master|REBASE
,那么你可以直接执行 git rebase --abort
。这会直接放弃本次 rebase。
其它的情况,例如 cherry-pick 也是同理。
但是,假如你的分支变成了 ((0903e23230...))
或者其它你看不懂的样子,并且 commit 也不见了一些,别慌!!
git 其实为我们保存了所有的历史记录。
我们使用 git reflog 可以查看:
想退出此模式的话直接 q
或者 wq
。
假设现在我想回到下面红框时候的 commit 状态:
此时,我只需要先复制它的 hash ------ c7d7bbb,然后 reset:
bash
git reset --hard c7d7bbb
就能回到最初的时候了!~
不过使用 --hard
参数会丢失暂存区的更改和影响工作目录,如果你不是很明确地想要回退到某个历史记录上,建议使用 git reset --soft
。下面也列一下 reset 的参数:
参数 | 作用描述 |
---|---|
--soft |
仅移动分支指针到指定提交,暂存区和工作目录的内容保持不变,不改变文件的修改状态,暂存的内容依然保留。 |
--mixed |
默认参数,移动分支指针到指定提交,同时重置暂存区,使其与指定提交时的状态一致,但工作目录中的文件内容不会被修改。 |
--hard |
移动分支指针到指定提交,并将暂存区和工作目录的内容都重置为指定提交时的状态,会覆盖未提交的修改,使用需谨慎。 |
--merge |
用于处理合并冲突后,将分支指针重置回合并前的状态,同时保留工作目录和暂存区中已解决的冲突内容。 |
--keep |
尝试将分支指针移动到指定提交,同时保留工作目录中的修改。若工作目录中的修改与指定提交存在冲突,重置操作会失败。 |
--abort |
终止一个正在进行的--merge 或--keep 重置操作。 |
给个三连吧!!!!!!!!!!!!
往期推荐
爆肝两个月,我用flutter开发了一款免费音乐app 80+
👍🏻 102+
💚
搭建一个快速开发油猴脚本的前端工程 24+
👍🏻 42+
💚
金九银十招聘季,IT 打工人,该怎么识别烂公司好公司? 70+
👍🏻 80+
💚
别人休息我努力,悄悄写个 cli 工具,必须提升效率,skr~ 60+
👍🏻 110+
💚
一文掌握 eslint,再也不怕项目报错 20+
👍🏻 30+
💚
开发一个 npm 库应该做哪些工程配置? 40+
👍🏻 50+
💚
分享我在前端学习与开发中用到的神仙网站和工具 40+
👍🏻 110+
💚
uniapp 踩坑记录(二) 130+
👍🏻 150+
💚
闲来无事,摸鱼时让 chatgpt 帮忙,写了一个 console 样式增强库并发布 npm 100+
👍🏻 110+
💚
uniapp 初体验踩坑记录 30+
👍🏻 60+
💚
两小时学会 JS 正则表达式,终身不忘 50+
👍🏻
【一年前端必知必会】如何写出简洁清晰的代码 50+
👍🏻
【一年前端必知必会】了解 Blob,ArrayBuffer,Base64 40+
👍🏻 90+
💚