🔥🔥🔥2.5W字!8个场景问题!带你了解最实用的 git 操作!!!

版权声明:本人文章仅在掘金平台发布,请勿抄袭搬运,转载请注明作者及原文链接 🦉

阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉

网上稍微搜一搜,每一篇 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 stashgit 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+ 💚

为什么就这个文件的 ESLint 检查失效了?

学会 TypeScript 体操,轻松看懂开源项目代码

别人休息我努力,悄悄写个 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+ 💚

相关推荐
lemonzoey3 分钟前
用Web Worker优化大文件上传——告别页面卡顿!
前端·javascript
Json_5 分钟前
Vue 内置组件 -slot讲解
前端·vue.js·深度学习
祯民16 分钟前
《生成式 AI 应用开发:基于 OpenAI API 开发》实体书上架
前端·aigc·openai
bigyoung21 分钟前
ts在运行时校验数据类型的探索
前端·javascript·typescript
独立开阀者_FwtCoder25 分钟前
深入解密Node共享内存:这个原生模块让你的多进程应用性能翻倍
前端·javascript·后端
Json_26 分钟前
使用JS写一个用鼠标拖动DIV到任意地方
前端·javascript·深度学习
祯民31 分钟前
阿民解锁了"入职 30 天跑路"新成就
前端·面试
昌平第一王昭君32 分钟前
一个简单的虚拟滚动
前端
Json_34 分钟前
jQuery选项卡小练习
前端·深度学习·jquery
王sir万岁38 分钟前
普通前端工程师如何入门 Web3 开发?
前端