在日常开发中,我们经常会在自己的功能分支上进行多次提交,这些提交可能包含了很多"修复typo"、"调整格式"等杂乱的中间提交。当准备向上游分支提交PR时,将这些杂乱的commit压缩成一个干净、整洁的commit是一个好习惯。本文将介绍如何使用git reset --soft来实现这一目标。

场景说明

假设我们有以下场景:

  • 本地开发分支feat-dev
  • 上游目标分支upstream/develop
  • 目标:将feat-dev分支相对于upstream/develop的所有commit压缩成一个commit

完整操作步骤

第一步:确保代码已提交

在开始操作前,确保当前工作区是干净的:

1
git status

如果有未提交的更改,请先提交或暂存。

第二步:更新上游分支(重要!)

在压缩提交之前,强烈建议先更新本地的上游分支,以避免后续合并时产生冲突:

1
2
3
4
5
# 获取上游仓库的最新代码
git fetch upstream

# 或者如果你没有设置upstream remote,可能是origin
git fetch origin

第三步:合并上游分支到开发分支(推荐)

将上游分支的最新代码合并到你的开发分支中,提前解决可能的冲突:

1
2
3
4
5
6
7
8
9
# 确保当前在feat-dev分支
git checkout feat-dev

# 将上游分支合并到当前分支
git merge upstream/develop

# 如果有冲突,在此处解决冲突后提交
# git add .
# git commit -m "resolve merge conflicts"

第四步:创建备份分支(可选但推荐)

以防万一操作失误,建议先创建一个备份分支:

1
git branch feat-dev-backup

第五步:查看要压缩的提交数量

可以先查看有多少个提交需要被压缩:

1
2
3
4
5
6
7
8
9
# 查找两个分支的共同祖先
git merge-base feat-dev upstream/develop

# 假设返回的commit hash是 97de0a079038a631719408d1a5c3b0b4db8c593a
# 统计要压缩的提交数量
git log --oneline 97de0a079038a631719408d1a5c3b0b4db8c593a..feat-dev | wc -l

# 或者直接查看这些提交
git log --oneline upstream/develop..feat-dev

第六步:执行软重置

使用git reset --soft将HEAD重置到上游分支,但保留所有更改在暂存区:

1
git reset --soft upstream/develop

说明--soft参数的作用是只移动HEAD指针,不改变暂存区和工作区的内容。这意味着所有的代码更改都会被保留在暂存区中,等待你创建新的提交。

第七步:创建新的压缩提交

现在所有的更改都在暂存区中,创建一个新的提交:

1
git commit -m "feat: 实现xxx功能"

第八步:验证结果

确认压缩操作成功:

1
2
3
4
5
# 查看最近的提交历史
git log --oneline --graph -5

# 查看相对于上游分支的提交(应该只有一个了)
git log --oneline upstream/develop..feat-dev

一键脚本

为了方便日常使用,这里提供一个一键完成的Shell脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/bin/bash

# Git Commit 压缩脚本
# 功能:将当前分支相对于目标分支的所有提交压缩成一个
# 用法:./squash-commits.sh <目标分支> <提交信息>
# 示例:./squash-commits.sh upstream/develop "feat: 实现用户登录功能"

set -e # 遇到错误立即退出

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 检查参数
if [ $# -lt 2 ]; then
echo -e "${RED}用法: $0 <目标分支> <提交信息>${NC}"
echo -e "${YELLOW}示例: $0 upstream/develop \"feat: 实现用户登录功能\"${NC}"
exit 1
fi

TARGET_BRANCH=$1
COMMIT_MSG=$2
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)

echo -e "${GREEN}========== Git Commit 压缩工具 ==========${NC}"
echo -e "当前分支: ${YELLOW}${CURRENT_BRANCH}${NC}"
echo -e "目标分支: ${YELLOW}${TARGET_BRANCH}${NC}"
echo ""

# 检查工作区是否干净
if [ -n "$(git status --porcelain)" ]; then
echo -e "${RED}错误: 工作区不干净,请先提交或暂存更改${NC}"
exit 1
fi

# 检查目标分支是否存在
if ! git rev-parse --verify "$TARGET_BRANCH" > /dev/null 2>&1; then
echo -e "${RED}错误: 目标分支 ${TARGET_BRANCH} 不存在${NC}"
exit 1
fi

# 获取远程最新代码
echo -e "${GREEN}[1/6] 正在获取远程最新代码...${NC}"
REMOTE_NAME=$(echo "$TARGET_BRANCH" | cut -d'/' -f1)
git fetch "$REMOTE_NAME" 2>/dev/null || echo -e "${YELLOW}警告: 无法获取远程 ${REMOTE_NAME} 的最新代码${NC}"

# 统计要压缩的提交数量
COMMIT_COUNT=$(git log --oneline "$TARGET_BRANCH".."$CURRENT_BRANCH" | wc -l)
echo -e "${GREEN}[2/6] 发现 ${YELLOW}${COMMIT_COUNT}${GREEN} 个提交需要压缩${NC}"

if [ "$COMMIT_COUNT" -eq 0 ]; then
echo -e "${YELLOW}没有需要压缩的提交${NC}"
exit 0
fi

# 显示要压缩的提交
echo -e "${GREEN}这些提交将被压缩:${NC}"
git log --oneline "$TARGET_BRANCH".."$CURRENT_BRANCH"
echo ""

# 创建备份分支
BACKUP_BRANCH="${CURRENT_BRANCH}-backup-$(date +%Y%m%d%H%M%S)"
echo -e "${GREEN}[3/6] 创建备份分支: ${YELLOW}${BACKUP_BRANCH}${NC}"
git branch "$BACKUP_BRANCH"

# 合并目标分支(如果有更新)
echo -e "${GREEN}[4/6] 合并目标分支的最新代码...${NC}"
if git merge "$TARGET_BRANCH" --no-edit 2>/dev/null; then
echo -e "${GREEN}合并成功${NC}"
else
echo -e "${YELLOW}合并过程中发生冲突,请手动解决后重新运行此脚本${NC}"
echo -e "${YELLOW}备份分支已创建: ${BACKUP_BRANCH}${NC}"
exit 1
fi

# 执行软重置
echo -e "${GREEN}[5/6] 执行软重置...${NC}"
git reset --soft "$TARGET_BRANCH"

# 创建新提交
echo -e "${GREEN}[6/6] 创建新的压缩提交...${NC}"
git commit -m "$COMMIT_MSG"

# 显示结果
echo ""
echo -e "${GREEN}========== 操作完成 ==========${NC}"
echo -e "原有 ${YELLOW}${COMMIT_COUNT}${NC} 个提交已压缩为 1 个"
echo -e "备份分支: ${YELLOW}${BACKUP_BRANCH}${NC}"
echo ""
echo -e "${GREEN}最终提交记录:${NC}"
git log --oneline -3

echo ""
echo -e "${GREEN}相对于目标分支的提交:${NC}"
git log --oneline "$TARGET_BRANCH".."$CURRENT_BRANCH"

echo ""
echo -e "${YELLOW}提示: 如需恢复,请执行: git reset --hard ${BACKUP_BRANCH}${NC}"

脚本使用方法

  1. 将上述脚本保存为 squash-commits.sh

  2. 添加执行权限:

    1
    chmod +x squash-commits.sh
  3. 执行脚本:

    1
    ./squash-commits.sh upstream/develop "feat: 实现用户登录功能"

脚本功能说明

该脚本会自动完成以下操作:

  1. ✅ 检查工作区是否干净
  2. ✅ 获取远程仓库的最新代码
  3. ✅ 统计并显示要压缩的提交数量
  4. ✅ 创建带时间戳的备份分支
  5. ✅ 合并目标分支的最新代码(提前解决冲突)
  6. ✅ 执行软重置
  7. ✅ 创建新的压缩提交
  8. ✅ 显示操作结果

原理解释

为什么使用 git reset --soft

Git reset 有三种模式:

模式 作用 HEAD 暂存区 工作区
--soft 只移动HEAD ✅改变 ❌不变 ❌不变
--mixed(默认) 移动HEAD和暂存区 ✅改变 ✅清空 ❌不变
--hard 全部重置 ✅改变 ✅清空 ✅清空

使用--soft模式,所有的代码更改都保留在暂存区,只需要一个git commit就能将所有更改合并为一个提交。

git rebase -i 的对比

传统上,我们也可以使用交互式rebase来压缩提交:

1
git rebase -i upstream/develop

然后在编辑器中将除第一个以外的所有pick改为squashs

两种方法的对比:

特性 reset --soft rebase -i
操作复杂度 简单,一条命令 复杂,需要编辑器操作
保留提交信息 需要重新编写 可以合并原有信息
冲突处理 一次性处理 可能多次处理
适用场景 所有提交压缩为一个 灵活选择压缩哪些

注意事项

  1. 不要在公共分支上执行此操作:这会改变提交历史,如果其他人基于原有提交工作,会造成问题。

  2. 强制推送:压缩提交后,如果之前已经推送过,需要强制推送:

    1
    git push -f origin feat-dev
  3. 备份很重要:虽然有reflog可以恢复,但创建备份分支更加直观和安全。

  4. 合并前先更新:先合并目标分支的最新代码,可以提前发现并解决冲突,避免提交PR后才发现问题。

总结

将多个杂乱的开发提交压缩成一个干净的提交,是提高代码仓库整洁度的好习惯。使用git reset --soft方法简单直接,配合本文提供的自动化脚本,可以大大简化这一操作流程。

记住三个关键步骤:

  1. 先更新 - 获取并合并上游最新代码
  2. 备份 - 创建备份分支以防万一
  3. 软重置后提交 - 使用git reset --soft后创建新提交