Skip to content

Git 实用技巧

作者:Atom
字数统计:8k 字
阅读时长:28 分钟

Git 中有很多实用的技巧, 让提交代码变成更愉悦的事, 本文将持续记录作者积累的一些技巧

~^ 的区别

~^ 都是 Git 的提交引用修饰符, 用于从某个已知提交(如 HEAD 或某个 commitId)出发, 定位它的祖先提交。两者最容易混淆, 一句话先记住核心区别:

  • ^ 选父提交: 一个提交若有多个父(合并提交), ^1 取第一个父, ^2 取第二个父。横向在同一代的多个父之间做选择
  • ~ 回溯代数: 沿着第一父这条线一路往上走, ~n 表示回退 n 代, 每一步都只走第一父。纵向沿主线往回数

因此对普通的线性提交(只有一个父), ~1^1 完全等价; 只有遇到合并提交时, 两者才会分道扬镳。

Commit Log

avatr

每条线上的星号(*)右侧是对应的 commit id, 开始和结尾交叉代表公共提交。可以使用参数--decorate--graph来简化显示 Git 提交历史

sh
git log --graph --decorate

上图第一条线拥有 165f5a1->3cb9272->de87e10->b6de943这几条提交记录:

下面是等价的表示方法:

git-graph.png

rust
A = = A^0
B = A^ = A^1 = A~1
C = A^2
D = A^^ = A^1^1 = A~2
E = B^2 = A^^2
F = B^3 = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2 = B^^2 = A^^^2 = A~2^2
I = F^ = B^3^ = A^^3^
J = F^2 = B^3^2 = A^^3^2

这张对照表里最值得记住的是 A^2A~2 含义完全不同:

  • A^2 = A 的第二个父提交, 只在 A 是合并提交时才存在
  • A~2 = A^1^1 = 沿第一父往上数两代(相当于 A 的"祖父")

初学者常把 ^2 误当成"往上两代", 实际上"往上两代"应写作 ~2^^

HEAD^[num]

^[num] 用于在合并提交处选择第几个父。以上面字母图为例, A 是一个合并提交, 有多个父:

sh
# A^1 = B(第一父), A^2 = C(第二父)
git reset --hard HEAD^2

HEAD^2 取的是 A第二父 C。注意 C 位于「被合并进来」的那条分支上, 和第一父 B 所在的主线是两条不同的线, 因此它们后续的 commit id 自然对不上——这正是 ^2^1/~1 的关键分野。省略数字时 HEAD^ 等价于 HEAD^1

HEAD~[num]

~[num] 用于沿第一父这条主线回退 n 代, 是日常最常用的写法:

sh
# A~1 = B, A~2 = D, A~3 = G……每一步都只走第一父
git reset --hard HEAD~1

HEAD~1 回到主线上一代 B; HEAD~2 再往上到 D。与 ^ 不同, ~ 永远不会拐进合并进来的旁支。

^ 可以连写多个, 每个 ^(不带数字时)都走第一父回退一代, 效果等同 HEAD~[num]:

sh
# HEAD^^ = HEAD^1^1 = HEAD~2 = D
git reset --hard HEAD^^

沿第一父连退两代, 到达字母图中的 D

HEAD~~

~ 同样可以连写, 每个 ~ 走第一父回退一代, 与 HEAD^^ 完全等价:

sh
# HEAD~~ = HEAD~2 = D
git reset --hard HEAD~~

结果同上, 到达 D。日常更推荐带数字的 HEAD~2 写法, 比连写一串符号更清晰。

如何选用

一个形象的记忆法: 波浪线 ~ 外观接近直线, 表示沿主线笔直往回走; 插入符 ^ 形似树杈或岔路口, 表示在合并处选择走哪一条父分支

需求用法说明
回退 n 代(最常见)HEAD~n沿第一父往上数 n 代
选合并提交的第几个父HEAD^n仅合并提交有第二父及以上
回退 1 代HEAD~1 = HEAD^ = HEAD^1三者等价
组合使用HEAD~2^2先沿主线回退 2 代, 再取该提交的第二父
  • 大部分时间用 ~: 回溯几代, 通常就是你想要的
  • ^ 用于合并提交: 因为它们有两个或更多(直接)父级, 需要明确指定走哪一条

pull 产生合并提交后, 如何精准还原到 pull 之前

~^ 最典型的实战场景, 就是 git pull 之后想撤掉自动产生的合并提交。这里藏着一个很多人踩过的坑。

场景

本地分支和远端同名分支产生分叉(各自都有新提交)时, 执行 git pull 因无法 fast-forward, Git 会自动创建一个合并提交(merge commit):

这个合并提交 M 有两个父:

  • 第一父 M^1(即 M~1) = pull 之前你本地的 HEAD(上图的 X)
  • 第二父 M^2 = pull 拉下来的远端 HEAD(上图的 Y)

正确做法: reset 到第一父

想丢弃这次合并、回到 pull 之前本地的样子, reset 到第一父即可:

sh
# ~1 与 ^1 在这里等价, 都稳稳指向合并前的本地 HEAD
git reset --hard HEAD~1
# 或显式写合并提交的 id
git reset --hard <merge-id>^1

Git 有一条稳定约定: 合并时当前分支(合并前的 HEAD)永远是第一父, 被合并进来的分支是第二父。所以 ~1 / ^1 一定落在 pull 之前的本地位置, 不会取到远端那一支。

陷阱: 别靠 git log 肉眼找"上一个 commit"

很多人不用 ~1, 而是 git log 看一眼合并提交下面紧挨着的那条, 复制它的 id 去 reset。这在合并场景下极易还原错

原因是: git log 默认按提交时间戳排序, 把合并的两条父分支"压平"后穿插显示, "紧挨着合并的下一行"和"第一父"没有必然关系——谁的时间新谁排前面。

举个真实例子, 某合并提交 M 的两个父:

commit时间含义
第一父 M^1c679dff07-08想要的 pull 之前的本地
第二父 M^2380ddac07-21选错 pull 拉下来的远端

因为远端父(07-21)比本地父(07-08)新, git log 按时间序把 380ddac 排在紧跟 M下一行, 但它其实是第二父(远端):

此时若 git reset --hard 380ddac, 还原到的是远端代码, 而不是 pull 之前你本地的 c679dff——方向正好反了。

可靠的核对方式:

sh
# 1. 直接用父指针, 不靠肉眼(最稳)
git reset --hard <merge-id>^1

# 2. 用 --graph 看清 | \ 的分叉结构, 分辨哪条是第一父
git log --graph --oneline -6 <merge-id>

# 3. 落点存疑时, 先 show 确认再 reset
git show <merge-id>^1

# 4. 用拓扑序代替默认时间序, 让第一父链紧跟合并显示, 缓解视觉误导
git log --topo-order --oneline -6 <merge-id>

补充根因: git log 默认近似按提交时间(--date-order)排列, 才会把时间更新的第二父排到前面; 加 --topo-order 会严格按拓扑序、优先展开第一父链, 视觉上更不容易选错。

如何查到第一父的真实 commit id

上面说「用 <merge-id>^1」是引用写法, 但很多时候你需要的是那个父提交实际的 40 位 / 短 id(比如记录下来、贴给同事、或写进脚本)。下面几条命令都能拿到。

方式一: 直接解析引用为 commit id(最快)

sh
# 解析合并提交的第一父, 输出完整 40 位 id
git rev-parse <merge-id>^1
# 加 --short 输出简短 id
git rev-parse --short <merge-id>^1
# 合并提交就是当前 HEAD 时, 可直接写
git rev-parse --short HEAD^1

方式二: 一次性列出某合并提交的所有父

sh
# %h=短id %p=所有父的短id %s=标题。%p 里空格分隔, 第一个即第一父
git show -s --format='commit=%h  parents=%p  msg=%s' <merge-id>
# 输出示例:
# commit=8fc5ab7  parents=c679dff 380ddac  msg=Merge branch ...
#                          ↑第一父   ↑第二父

parents= 后面按顺序列出父提交, 第一个就是第一父。这是分辨父顺序最直观的一条命令。

方式三: 只取第一父那条线的历史

sh
# --first-parent 只沿第一父链走, 忽略被合并进来的旁支
# 合并提交下面紧跟的那条, 就一定是第一父, 不再受时间序干扰
git log --first-parent --oneline -5 <merge-id>

如何在 --graph 里认出第一父

git log --graph 有一条结构规则: 合并提交的第一父继续留在合并所在的那一列(最左、不缩进), 其余父向右缩进另起一列(| *)。

但有个反直觉的坑: 默认按时间排序时, 时间更新的第二父那一整棵子树会先显示, 你往往要向下翻过一大段, 才在最左列看到第一父。所以判断依据是列(缩进), 不是离合并提交的远近

sh
git log --graph --oneline --decorate <merge-id>
*   8fc5ab7 Merge ...          <- 合并提交 M(最左列)
|\
| * 380ddac ...               <- 向右缩进的旁支 = 第二父(可能很长)
| * e59b706 ...                  第二父的历史会整段先列出……
* | c679dff ...               <- 回到 M 那一列(最左)= 第一父
|/
* 5c443d7 ...                 <- 两线重新汇合的共同祖先
  • 和 M 同列(最左、不缩进)的 * | = 第一父
  • 向右缩进的 | * = 第二父

因为第二父往往更新、会整段排在前面, "离 M 最近的那个星号"经常是第二父——别被距离骗了, 只认列

嫌数列麻烦, 就用 --first-parent 把所有旁支直接隐藏, 这样合并提交下面紧跟的必是第一父:

sh
git log --first-parent --oneline -5 <merge-id>

认出后, 再用 git rev-parse <merge-id>^1 取它准确的 commit id。

三个必须记住的前提

reset 之前先想清楚这三点

  1. 只还原本地, 远端不变: reset 只移动本地分支指针。远端 origin/feature 仍指向合并提交 M。要让远端也回退, 还得 git push --force(共享分支上高危, 会影响其他协作者), 详见文末回滚远端分支
  2. --hard 会丢弃未提交改动: 操作前确保工作区干净(git status 检查), 否则未提交的本地修改会一并丢失
  3. 前提是合并之后没再提交: 若合并后又累积了新提交, ~1 位置不变, 但 reset 会把合并及其之后的提交一并丢掉——只想撤合并、保留后续提交时不能这么做, 应改用 git revert -m 1 <merge-id>

一句话总结

~1 / ^1 靠谱, "git log 里看到的上一行"不靠谱; 合并场景下后者会大概率选反。撤 pull 合并, 认准第一父。

代码统计

如果想了解多人团队中同事或者自己的代码统计情况, git提供了相关的命令, 方便且直观

sh
# 查看团队每个人的代码提交量
git shortlog -s -n

Fast-Forward 合并

Git 中的 fast-forward(快速前进)是一种合并(merge)分支的方式,它通常用于将一个分支的更改合并到另一个分支上。

Fast-forward 合并的效果是,目标分支(主分支)的指针直接移动到源分支(开发分支)的最新提交,因此看起来就像是主分支“快速前进”到了开发分支的状态

Fast-forward 合并通常是一种简单且干净的合并方式,因为它不会创建合并提交(merge commit),但它只适用于特定的情况,即主分支没有新的提交。如果主分支有新的提交,你可能需要执行普通的合并,这将创建一个合并提交以整合来自其他分支的更改

Fast-forward 合并只会发生在特定条件下

  • 合并到主分支

假设你有一个开发分支,比如 feature-branch,并且你在该分支上做了一些更改。当你想将这些更改合并到主分支(通常是 master 分支)时,如果在合并时没有新的提交到主分支,Git 将执行 fast-forward 合并

  • 没有冲突

fast-forward 合并中,没有冲突会发生。这是因为在 fast-forward 合并时,Git 简单地将目标分支(主分支)指向源分支(开发分支)的最新提交,这不会导致冲突

更多相关的内容可以查阅Merging vs Rebasing

变基 Rebase

笔者总结了rebase的最常用三种用法(注意区间前开后闭):

合并 commit

在开发中, 可能自己的分支进行了多次的提交, 为了保持合入公共开发分支中自己的 commit 的整洁性, 可以使用 commit 合并

注意

一般使用场景是自己的本地分支, 最好不要在远端公共分支上进行这种操作, 是极其危险的

sh
# 只需给一个基点: 基点是待整理提交的父提交(前开后闭, 不含基点本身)
# 例如要整理最近 3 个提交, 基点写 HEAD~3
git rebase -i HEAD~3
# 若基点处于分离头指针, 可切出一个新分支保存结果
git checkout -b <name>
# 推送到自己的远端
git push origin HEAD

修改 commit

如果前几次已经git commit到本地, 此时想修改他们提交信息, 也可以是用rebase -i命令

sh
# 基点是待修改提交的父提交。例如修改最近一次提交, 基点即 HEAD~1
git rebase -i <base-commit>

此时在vim模式下, 与合并commit所选择使用的pick选项不同, 而是选择使用reword选项来修改提交信息。

rebase -i 支持的选项有下图中的几种

修改提交信息

特殊情况

如果只修改最新一次的提交信息, 可以使用git commit --amend命令

此时等同于git rebase -i HEAD~1

复制 commit

可以复制某一个分支的一串 commit 到当前分支, 类似于cherry-pick

  • base: 分支名称
  • from: 待合并片段的起始 commitId(不包含)
  • to: 待合并片段的结束 commitId(包含)

语法:

git rebase --onto base from to

sh
# 被复制的分支上执行
git checkout -b  <new-branch>  <end-commit>

git rebase --onto  <target-branch>  <start-commit>^

合并远端分支

拉取远端代码的具体分支时, 拉取后与本地进行fast-forward合并, 可以使用

sh
git pull --rebase origin <target-branch>

重设分支基点

团队协作中, 主分支经常会更新一些内容, 因此我们提交到主分支前一般都需要先合并主分支的内容到开发分支, 主要使用两种方式合并

如果使用merge来合并主分支, 那么会产生一条多余的无关合并记录, 从而污染了开发分支, 如图

Merging-main-into-feature.svg

为了解决上述的问题, 就得使用rebase重设开发分支的基点, 使得开发分支的提交记录变得干净

sh
git checkout feature
git rebase main

这样就将整个feature分支从分支的顶端开始,有效地将所有新提交合并到main。 但是,变基不是使用合并提交,而是通过为原始分支中的每个提交创建全新的提交来重写项目历史记录

合并后的分支图如下

Rebasing-the-feature-branch-into-main.svg

Rebase vs Merge 对比

RebaseMerge
历史线线性, 干净保留分叉和合并节点
commit 记录看起来像在 main 最新节点上顺序开发会产生额外的 merge commit
冲突解决逐个 commit 解决, 可能需要多次一次性解决所有冲突
可追溯性原始分支点信息丢失保留完整的分支合并拓扑
commit SHA会改变(历史改写)不变

开源项目中的 Rebase 工作流

大多数开源项目(如 Linux kernel)要求贡献者在提交 PR 前先 rebase main, 以保持主分支历史的线性和干净。典型流程如下:

sh
# 1. Fork 并 clone 项目后, 从 main 拉出特性分支
git checkout main
git pull origin main
git checkout -b feature/my-feature

# 2. 在特性分支上开发, 产生多个 commit
git commit -m "feat: add X"
git commit -m "fix: adjust Y"

# 3. 开发完成, 合入前先 rebase 远端最新的 main
git fetch origin
git rebase origin/main

# 4. 如果有冲突, 逐个解决(rebase 会逐个 commit 重放)
git add .
git rebase --continue
# 重复直到所有 commit 重放完毕

# 5. 可选: 交互式 rebase 整理 commit, 把零碎提交合并成语义完整的提交
git rebase -i origin/main
# 在编辑器中将多个 commit 标记为 squash 或 fixup

# 6. 推送到自己的远程特性分支(历史被改写, 需要 force push)
git push --force-with-lease origin feature/my-feature

# 7. 创建 PR, 等待 review 后合入 main

黄金法则

只 rebase 自己的特性分支, 绝不 rebase 公共分支(如 main)

rebase 会改写 commit SHA。如果在 main 上执行 rebase, 所有基于 main 工作的协作者的本地历史都会与远程不一致, 导致大面积冲突

用图来理解这个原则:

关于 --force-with-lease

rebase 后需要 force push, 但应使用 --force-with-lease 而非 --force。前者会检查远程是否有别人的新推送, 更加安全

同步落后的本地开发分支

实际开发中经常遇到本地开发分支同时落后于远端同名分支和 main 分支的情况:

标准处理流程如下:

Step 1: 拉取远端最新信息

sh
# fetch 只拉取元数据, 不修改本地工作区, 永远安全
git fetch origin

Step 2: 同步远端同名分支的最新提交

sh
# 先合入协作者在同一分支上的新提交
git rebase origin/feature/xxx

Step 3: 同步 main 分支的最新变更

sh
# 将开发分支的基点重设到最新的 main
git rebase origin/main

Step 4: 解决冲突(如有)

sh
# 逐个解决冲突文件
git add <resolved-file>
git rebase --continue

# 如果冲突太复杂想放弃
git rebase --abort

Step 5: 推送到远端

sh
# rebase 改写了历史, 需要 force-push
# --force-with-lease 比 --force 安全: 远端有未见过的新提交时会拒绝推送
git push --force-with-lease origin feature/xxx

速记命令(个人分支场景):

sh
git fetch origin && git rebase origin/main && git push --force-with-lease

个人分支 vs 共享分支

  • 个人特性分支 → 用 rebase, 保持历史线性
  • 多人共享分支 → 用 merge, 避免改写他人历史

如果是共享分支, 将上述 rebase 替换为 merge:

sh
git fetch origin
git merge origin/main
git push origin feature/xxx

删除本地无效的分支

远端有很多分支已经被删除, 而本地仍然存在, 删除本地的无效的分支可以使用prune命令

sh
# 列出已经失效的引用分支
git remote prune show  origin

# 删除失效的分支
git remote prune origin

恢复被删除的 stash 代码

有时候不小心清空了stash list中的备用代码, 想找回来怎么办? 可以使用以下命令:

sh
# 撤销git stash clear的操作
git fsck --unreachable | grep commit | cut -d ' ' -f3 | xargs git log --merges --no-walk --grep=WIP

文件换行符

windowsunix等各种系统上面采用了不同的换行符, 换行符的统一在团队合作时尤为关键

查看换行符

如果未统一换行符,那么必然会导致一些问题, 下面的命令可以检索出所有文件的换行符类型

详细的列出当前工作目录中所有文件的换行符类型, 总的来说, 换行符有以下几种常见格式

  • LFUnix 风格的换行符(\n)
  • CRLFWindows 风格的换行符(\r\n)
  • CRMac OS 风格的换行符(\r)
  • none:二进制文件或没有换行符的文件
sh
git ls-files --eol

换行符配置

跨平台协作开发是常有的,不统一的换行符确实对跨平台的文件交换带来了麻烦。最大的问题是,在不同平台上,换行符发生改变时,Git 会认为整个文件被修改,这就造成我们没法 diff,不能正确反映本次的修改。还好 Git 在设计时就考虑了这一点,其提供了一个 autocrlf 的配置项,用于在提交和检出时自动转换换行符,该配置有三个可选项:

  • true: 提交时转换为 LF,检出时转换为 CRLF
  • false: 提交检出均不转换
  • input: 提交时转换为LF,检出时不转换

用如下命令即可切换三种配置

sh
# 提交时转换为LF,检出时转换为CRLF

# 全局配置
git config --global core.autocrlf true
# 局部配置
git config --local core.autocrlf true
sh
# 提交时转换为LF,检出时不转换

# 全局配置
git config --global core.autocrlf input
# 局部配置
git config --local core.autocrlf input
sh
# 提交检出均不转换

# 全局配置
git config --global core.autocrlf false
# 局部配置
git config --local core.autocrlf false

如果把 autocrlf 设置为 false 时,那另一个配置项 safecrlf 最好设置为 ture。该选项用于检查文件是否包含混合换行符,其有三个可选项:

  • true: 拒绝提交包含混合换行符的文件
  • false: 允许提交包含混合换行符的文件
  • warn: 提交包含混合换行符的文件时给出警告

用如下命令即可切换三种配置

sh
# 拒绝提交包含混合换行符的文件

# 全局配置
git config --global core.safecrlf true
# 局部配置
git config --local core.safecrlf true
sh
# 允许提交包含混合换行符的文件

# 全局配置
git config --global core.safecrlf false
# 局部配置
git config --local core.safecrlf false
sh
# 提交包含混合换行符的文件时给出警告

# 全局配置
git config --global core.safecrlf warn
# 局部配置
git config --local core.safecrlf warn

统一换行符

如果想要统一换行符, 可以借助Prettier

sh
# 直接通过prettier运行转换操作, 点为要修改的文件路径,可以改成对应的目录
npx prettier --write --end-of-line lf .

在工程应用中, 更推荐的方式是使用Vscode插件 EditorConfig for VS Code , 并在根目录创建配置文件.editorconfig, 然后搭配EslintPrettier等工具, 完美解决换行符问题

想了解更多请参考作者的另一篇文章 代码风格工具集成

sh
# .editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

Git Reset 的三个选项

  • mixed(默认值)

回退一个版本,且会将暂存区的内容和本地已提交的内容全部恢复到未暂存的状态,不影响原来本地文件及未提交的本地修改

sh
git reset (-–mixed) HEAD~1
  • soft

回退一个版本,不清空暂存区,将已提交的内容恢复到暂存区,不影响原来本地的文件(未提交的也不受影响)

sh
git reset -–soft HEAD~1
  • hard

回退一个版本,清空暂存区,将已提交的内容的版本恢复到本地,本地的文件也将被恢复的版本替换

sh
git reset -–hard HEAD~1

Git 恢复误删的分支

  • 使用 git log -g 找回之前提交的 commit_id

  • 使用 git branch recover_branch[新分支] commit_id 命令用这个 commit 创建一个分支

  • 切换到 recover_branch_abc 分支,检查文件是否存在

Git 图片提交失败

在某些情况下, Git提交成功并推送到远端后, 发现远端分支的图片并没有更新, 这种情况下需要使用-- force 或者 -- refresh选项来进行add

sh
git add --refresh .
# 或者
git add --force .

Git Tag 操作

Git tag 是给特定的提交打上标签,通常用于标记版本发布点。以下是常用的 tag 操作技巧:

创建标签

sh
# 创建轻量标签(推荐用于临时标记)
git tag <tagname>

# 创建带注释的标签(推荐用于版本发布)
git tag -a <tagname> -m "版本说明信息"

# 给特定的提交创建标签
git tag -a <tagname> <commit-id> -m "版本说明信息"

查看标签

sh
# 查看所有标签
git tag

# 查看标签详细信息
git show <tagname>

# 查看标签列表(按时间排序)
git tag --sort=-creatordate

# 查看标签列表(按版本号排序)
git tag --sort=version:refname

推送标签

sh
# 推送特定标签到远程仓库
git push origin <tagname>

# 推送所有标签到远程仓库
git push origin --tags

# 推送所有标签并覆盖远程标签
git push origin --tags --force

删除标签

sh
# 删除本地标签
git tag -d <tagname>

# 删除远程标签
git push origin --delete <tagname>

# 或者使用空标签覆盖远程标签
git push origin :refs/tags/<tagname>

检出标签

sh
# 检出标签到新分支(推荐方式)
git checkout -b <branch-name> <tagname>

# 直接检出标签(分离头指针状态)
git checkout <tagname>

标签管理最佳实践

版本标签命名规范

  • 使用语义化版本号:v1.0.0v2.1.3
  • 预发布版本:v1.0.0-alpha.1v1.0.0-beta.2
  • 发布候选版本:v1.0.0-rc.1
sh
# 示例:创建主版本标签
git tag -a v1.0.0 -m "第一个正式版本发布"

# 示例:创建预发布版本标签
git tag -a v1.1.0-beta.1 -m "Beta版本,新增用户管理功能"

# 示例:创建热修复版本标签
git tag -a v1.0.1 -m "修复登录验证bug"

删除远端库敏感文件

有时候不小心将一些敏感文件提交到远端库, 这时候需要删除远端库该敏感文件所有的记录

第一步、删除本地记录

首先在本地操作历史记录, 用于删除本地库中该文件所有记录

如果是文件夹, 还需要在git rm中添加-r参数

sh
git filter-branch --force --index-filter \
    "git rm --cached --ignore-unmatch 待删除的文件(相对项目的路径)" \
    --prune-empty --tag-name-filter cat \
    -- --all

第二步、将记录覆盖到远端

将上一步的操作覆盖到远端, 将远端库中的记录也全部删除

sh
# 覆盖所有的分支
git push origin --force --all
# 覆盖所有的tags
git push origin --force --tags

第三步、解除引用和垃圾回收

最后一步, 用于强制解除对本地存储库中的所有对象的引用和垃圾收集, 删除垃圾节省空间

sh
git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin
git reflog expire --expire=now --all
git gc --prune=now

回滚本地分支

若要撤销的是 git pull 自动产生的合并提交, 请优先看 pull 产生合并提交后, 如何精准还原到 pull 之前, 那里说明了为何要 reset 到第一父、以及 git log 选错父的陷阱。

git reset (--mixed) [commit id]

回退到指定版本,且会将暂存区的内容和本地已提交的内容全部恢复到未暂存的状态,不影响原来本地文件(未提交的也不受影响) 该参数--mixedgit reset的默认参数 例如需要回退一个版本, 执行下面命令:

sh
# 回退一个版本
git reset HEAD~1

需要回退到某个 commit id(通过 git log 查看), 如需要回滚到4a50c9f,则执行

sh
git reset 4a50c9f

git reset --soft [commit id]

回退到指定版本,不清空暂存区,将已提交的内容恢复到暂存区,不影响原来本地的文件(未提交的也不受影响)

git reset --hard [commit id]

回退到指定版本,清空暂存区,将已提交的内容的版本恢复到本地,本地的文件也将被恢复的版本替换

git revert [commit id]

生成一个新的 commit,将指定的 commit 内容从当前分支上撤除

常见用法

  • 撤销单个commit
sh
git revert commitId
  • 撤销多个不连续commit
sh
git revert commitId1 commitId2 commitId3
  • 撤销连续多个commit
sh
# 前开后闭区间, 不包含commitId1, 但包含commitId2
git revert commitId1..commitId2

git revert -m [parent Id][commit id]

当需要 revert 一个合并提交时, 因为它有多个父, Git 无法自动判断该以哪一条为主干, 必须用 -m 指定保留第几个父作为主线; 可以通过 git show [提交id] 查看该提交有几个父。

-m 1 表示以第一父为主干保留, 撤销由第二父(被合并进来的那条分支)引入的改动——这也是最常用的选择:

sh
# 保留第一父为主干, 撤销合并从第二父带来的变更
git revert -m 1 cad132423

回滚远端分支

案例: 需要回滚 master 上面的代码到 4a50c9f

第一种方法

主要使用Merge假合并策略, 以下是步骤:

  • 先回滚到需要移除的 commit id 的前一次正确 commit id
sh
git checkout -b remote-v1 4a50c9f
  • 合并策略为强行保留现在的分支(假合并) 合并中完全采用 remote-v1 的代码
sh
git merge -s ours master
  • 推送到远程分支
sh
# 也可以使用 git push origin HEAD:master
git push origin remote-v1:master

第二种方法

主要使用Revert撤销, 以下是步骤:

  • 先移除有代码错误的 commit id, 撤销一连串的 id 用(commit1..commit2](前开后闭区间, 不包含commit1, 但包含commit2), 参数--no-commit是用于后面手动提交
sh
# -n 是 --no-commit 的缩写
git revert -n f7742cd..551c408
  • 正常提交代码
sh
git commit -a -m 'This reverts commit 7e345c9 and 551c408'
git push origin HEAD:master

第三种方法(极不推荐) 危险

这种方式是强制操作, 忽略所有警告和报错, 强行推送并覆盖远端分支的代码

危险系数高, 稍微不慎, 会导致其他人的代码丢失, 以下是步骤:

  • 使用文章开头的方式回滚本地分支
  • 强行提交到远端分支
sh
git push origin master -f

参考资料