用脚本固化 Git Squash 合并与文件排除流程

在日常开发中,我们经常需要把一个功能分支合并到主分支,同时希望最终只生成一个提交记录。

常见操作是:

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"

这套流程的含义是:

  1. 使用 git merge --squash 把目标分支的改动合并到当前工作区和暂存区。
  2. 使用 git restore 把指定文件恢复到当前分支原始状态。
  3. 最后统一提交一次。

最终效果是:

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 合并实践。

相关推荐
wunaiqiezixin3 小时前
git常用命令总结
git
Pluchon8 小时前
萌萌技术分享笔记——Java综合项目
java·开发语言·笔记·git·github·mybatis·postman
九思x9 小时前
Git脚本汇总
git
jiayong239 小时前
git分支合并的切换逻辑详解
git
思麟呀9 小时前
Git入门
git
Ws_9 小时前
Git + Gerrit 第八课:reset 与 revert 撤销提交
git
Qres8219 小时前
hexo博客上传github page
git·github·hexo
繁星星繁10 小时前
Git 入门之道:从版本流转到基础操作
大数据·git·elasticsearch
wh_xia_jun1 天前
Git 分支合并操作备忘录
git