在日常开发中,我们经常需要把一个功能分支合并到主分支,同时希望最终只生成一个提交记录。
常见操作是:
bash
git merge --squash feature/foo
git commit -m "feat: add foo feature"
这种方式可以把 feature/foo 分支上的多个提交压缩成一个提交,保持主分支历史简洁。
但实际项目中经常会遇到一个问题:
我想合并功能分支的大部分代码,但不想合并某些文件。
例如:
text
.env
config/local.yml
package-lock.json
dist/
这些文件可能是本地配置、自动生成文件、环境相关文件,或者不希望被目标分支覆盖的文件。
这时,推荐使用:
text
git merge --squash + 排除清单 + 显式脚本
这是从灵活度、可操作性、可固化脚本三个角度综合来看最平衡的方案。
推荐方案
核心流程如下:
bash
git merge --squash <target-branch>
git restore --source=HEAD --staged --worktree --pathspec-from-file=<exclude-file>
git commit -m "<message>"
例如:
bash
git merge --squash feature/foo
git restore --source=HEAD --staged --worktree --pathspec-from-file=.git/info/squash-exclude
git commit -m "feat: add foo feature"
这套流程的含义是:
- 使用
git merge --squash把目标分支的改动合并到当前工作区和暂存区。 - 使用
git restore把指定文件恢复到当前分支原始状态。 - 最后统一提交一次。
最终效果是:
text
合并 feature/foo 的整体改动
但排除 squash-exclude 中声明的文件
并生成一个干净的 squash commit
为什么推荐 merge --squash
相比其他方案,merge --squash 更适合"整体合并一个分支,但排除部分文件"的场景。
优点一:合并语义清晰
bash
git merge --squash feature/foo
它表达的含义非常明确:
把 feature/foo 分支的最终差异压缩合并到当前分支。
它不像 cherry-pick 那样逐个提交应用,因此更接近正常合并流程,也更容易被团队成员理解。
优点二:冲突处理更简单
使用 merge --squash 时,冲突通常集中在一次合并过程中处理。
而使用 cherry-pick --no-commit 时,如果目标分支有多个提交,可能在多个提交上反复产生冲突。
例如:
bash
git cherry-pick --no-commit commit1
git cherry-pick --no-commit commit2
git cherry-pick --no-commit commit3
每一步都可能冲突。
对于团队常规合并来说,merge --squash 的操作体验更稳定。
优点三:更适合脚本固化
merge --squash 的流程比较固定:
text
检查工作区
执行 squash merge
根据排除清单恢复文件
提交
这非常适合封装成统一脚本。
团队成员不需要记住复杂命令,只需要执行:
bash
./scripts/squash-merge-exclude.sh feature/foo "feat: add foo feature"
即可完成标准化操作。
优点四:对包含 merge commit 的分支更友好
如果功能分支中包含 merge commit,cherry-pick 会比较麻烦。
例如:
bash
git cherry-pick --no-commit <merge-commit>
可能会报错:
text
error: commit xxx is a merge but no -m option was given.
因为 cherry-pick 合并提交时需要指定 mainline:
bash
git cherry-pick -m 1 --no-commit <merge-commit>
这会增加脚本复杂度和使用风险。
而 git merge --squash 直接基于分支最终差异进行合并,更适合常规工程流程。
排除清单设计
可以把排除文件列表放在:
bash
.git/info/squash-exclude
示例内容:
text
.env
config/local.yml
package-lock.json
dist/
.git/info/squash-exclude 是本地 Git 目录下的文件,不会被提交到仓库,适合保存个人本地规则。
如果希望团队共享排除规则,也可以放在仓库根目录:
bash
.squash-exclude
示例:
text
.env
config/local.yml
dist/
然后脚本中读取 .squash-exclude。
两种方式可以根据团队需要选择:
| 文件位置 | 是否进入版本管理 | 适用场景 |
|---|---|---|
.git/info/squash-exclude |
否 | 个人本地规则 |
.squash-exclude |
是 | 团队共享规则 |
如果是团队长期使用,推荐使用:
bash
.squash-exclude
这样规则透明、可审查、可维护。
核心命令解释
关键命令是:
bash
git restore --source=HEAD --staged --worktree --pathspec-from-file=.squash-exclude
拆开理解:
bash
--source=HEAD
表示从当前分支的 HEAD 恢复文件。
bash
--staged
表示恢复暂存区中的文件。
bash
--worktree
表示恢复工作区中的文件。
bash
--pathspec-from-file=.squash-exclude
表示从 .squash-exclude 文件中读取需要恢复的路径。
也就是说:
在 squash merge 后,把排除清单中的文件恢复成合并前的状态。
推荐脚本
可以在项目中创建脚本:
bash
scripts/squash-merge-exclude.sh
内容如下:
bash
#!/bin/sh
set -e
TARGET_BRANCH="$1"
COMMIT_MSG="$2"
EXCLUDE_FILE=".squash-exclude"
if [ -z "$TARGET_BRANCH" ]; then
echo "Usage: $0 <target-branch> [commit-message]"
exit 1
fi
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: working tree or index is not clean."
echo "Please commit or stash your changes first."
exit 1
fi
echo "Squash merging branch: $TARGET_BRANCH"
git merge --squash "$TARGET_BRANCH"
if [ -f "$EXCLUDE_FILE" ]; then
echo "Applying exclude file: $EXCLUDE_FILE"
git restore --source=HEAD --staged --worktree --pathspec-from-file="$EXCLUDE_FILE"
else
echo "No exclude file found: $EXCLUDE_FILE"
fi
echo "Current status:"
git status --short
if git diff --cached --quiet; then
echo "Nothing to commit after applying excludes."
exit 0
fi
if [ -n "$COMMIT_MSG" ]; then
git commit -m "$COMMIT_MSG"
else
git commit
fi
赋予执行权限:
bash
chmod +x scripts/squash-merge-exclude.sh
使用方式:
bash
./scripts/squash-merge-exclude.sh feature/foo "feat: add foo feature"
脚本执行流程
这个脚本主要做了几件事:
text
1. 校验是否传入目标分支
2. 检查当前工作区和暂存区是否干净
3. 执行 git merge --squash
4. 根据 .squash-exclude 排除指定文件
5. 检查是否还有可提交内容
6. 创建最终 squash commit
其中,工作区检查非常重要:
bash
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: working tree or index is not clean."
exit 1
fi
这可以避免把本地未提交改动混入 squash commit。
使用示例
假设当前在 main 分支,需要合并 feature/foo:
bash
git checkout main
准备排除文件:
bash
cat > .squash-exclude <<EOF
.env
config/local.yml
dist/
EOF
执行脚本:
bash
./scripts/squash-merge-exclude.sh feature/foo "feat: add foo feature"
最终会生成一个提交:
text
feat: add foo feature
其中包含 feature/foo 的主要改动,但不包含 .squash-exclude 中声明的路径变更。
也可以封装成 Git Alias
如果希望使用类似 Git 子命令的方式:
bash
git squash-merge-exclude feature/foo "feat: add foo feature"
可以配置 alias:
bash
git config alias.squash-merge-exclude '!f() {
branch="$1";
shift;
if [ -z "$branch" ]; then
echo "Usage: git squash-merge-exclude <branch> [commit-message]";
return 1;
fi;
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: working tree or index is not clean.";
return 1;
fi;
git merge --squash "$branch" || return 1;
if [ -f .squash-exclude ]; then
git restore --source=HEAD --staged --worktree --pathspec-from-file=.squash-exclude || return 1;
fi;
git status --short;
if git diff --cached --quiet; then
echo "Nothing to commit after applying excludes.";
return 0;
fi;
if [ "$#" -gt 0 ]; then
git commit -m "$*";
else
git commit;
fi
}; f'
使用:
bash
git squash-merge-exclude feature/foo "feat: add foo feature"
不过在团队协作中,更推荐把脚本放到仓库中,例如:
text
scripts/squash-merge-exclude.sh
这样更透明,也更容易审查和维护。
冲突处理
如果执行:
bash
git merge --squash feature/foo
时出现冲突,Git 会中断合并流程。
可以查看冲突文件:
bash
git status
解决冲突后:
bash
git add .
然后继续执行排除逻辑:
bash
git restore --source=HEAD --staged --worktree --pathspec-from-file=.squash-exclude
git commit -m "feat: add foo feature"
如果想放弃本次 squash merge:
bash
git merge --abort
如果 git merge --abort 不可用,也可以在确认没有重要本地改动的情况下执行:
bash
git reset --hard HEAD
和其他方案的简单对比
cherry-pick --no-commit
也可以通过下面方式实现类似效果:
bash
git cherry-pick --no-commit <commit1> <commit2>
git restore --source=HEAD --staged --worktree --pathspec-from-file=.squash-exclude
git commit -m "feat: add foo feature"
它的优点是灵活度高,可以精确选择某些 commit。
但缺点也明显:
- 多个 commit 可能多次冲突。
- 遇到 merge commit 时处理复杂。
- 脚本边界情况更多。
- 不如
merge --squash适合常规团队合并。
因此它更适合作为补充方案,而不是默认方案。
Git Hook
也可以用 hook 自动执行排除逻辑,例如在 merge 后自动恢复指定文件。
但 hook 有几个问题:
- 默认不会随仓库分发。
- 行为比较隐式。
- 团队成员不容易感知发生了什么。
- 不利于排查问题。
因此 hook 更适合个人本地习惯,不推荐作为团队主流程。
推荐度评估
综合比较:
| 方案 | 灵活度 | 可操作性 | 可固化脚本 | 推荐度 |
|---|---|---|---|---|
merge --squash + 排除清单 + 脚本 |
高 | 高 | 高 | 最高 |
cherry-pick --no-commit + 排除清单 |
很高 | 中 | 中 | 次选 |
| Git Hook 自动排除 | 中 | 高 | 中 | 不推荐作为团队主流程 |
最终推荐:
text
merge --squash + .squash-exclude + scripts/squash-merge-exclude.sh
最佳实践建议
1. 使用共享排除清单
团队项目建议使用:
bash
.squash-exclude
并提交到仓库。
这样所有人都能看到哪些文件会在 squash merge 时被排除。
2. 保持工作区干净
执行脚本前,确保没有未提交改动:
bash
git status
如果有本地改动,可以先提交或暂存:
bash
git stash
3. 不要排除核心业务代码
排除清单应主要用于:
text
本地配置文件
环境文件
构建产物
锁文件或自动生成文件
不要随意排除核心业务代码,否则容易导致合并结果不完整。
4. 脚本要显式执行
相比 hook,显式脚本更推荐:
bash
./scripts/squash-merge-exclude.sh feature/foo "feat: add foo feature"
因为它清晰、可见、可审查,适合团队长期维护。
总结
如果目标是:
text
把一个分支整体压缩合并到当前分支
但排除部分不希望合入的文件
并形成一个干净的提交记录
最推荐的方案是:
bash
git merge --squash <branch>
git restore --source=HEAD --staged --worktree --pathspec-from-file=.squash-exclude
git commit -m "<message>"
工程化后可以固化为:
bash
./scripts/squash-merge-exclude.sh feature/foo "feat: add foo feature"
这套方案兼顾了:
text
稳定性
可读性
可操作性
团队可维护性
脚本可固化能力
对于大多数团队来说,它比 cherry-pick --no-commit 更简单,比 Git Hook 更透明,是一种更适合长期使用的 Git Squash 合并实践。