Git分支清理技巧:解决Squash合并后的分支管理难题

本文详细介绍了在使用Git的Squash和Merge功能后,如何通过创建智能别名来自动清理已删除的远程分支。作者分享了完整的Git别名配置方案,解决了传统分支清理方法在压缩合并场景下失效的问题。

清理已删除的Git分支

很久以前,我编写了一套有用的Git别名来支持GitHub工作流。我最喜欢的别名是bdone,它会:

  • 切换到默认分支
  • 运行git up确保代码是最新的
  • 运行git bclean删除所有已合并到默认分支的分支

这个方法在很长一段时间内都工作得很好。它的工作原理是列出所有已合并到默认分支的分支,然后删除它们。当时我并没有使用git branch --merged来列出已合并的分支,因为那时我还不知道这个命令。

然而,在我加入PostHog后,我的别名最近停止工作了。主要原因是他们几乎在所有仓库中都使用Squash和Merge来合并PR。

当你使用git merge --squash或GitHub的"Squash and merge"功能时,Git会在目标分支上创建一个新的提交,将源分支的所有更改合并为单个提交。这个新提交不会保留来自源分支的原始提交的任何引用。因此,Git不会认为源分支已被合并,像git branch --merged这样的命令也不会显示它。

但关于Git的一点是:几乎总有解决办法。

解决方案

当你在GitHub上合并PR时,它会显示一个"Delete branch"按钮:

这是一个很棒的功能。它是清理已合并到默认分支的分支的好方法。事实上,你可以配置GitHub在合并时"自动删除头部分支":

我强烈建议你也这样做。当远程分支被删除时,Git会将其标记为"gone"。例如,如果你运行git branch -vv,你会看到类似这样的输出:

1
2
3
4
5
6
> git branch -vv

  haacked/decide-v4      ba39408 [origin/haacked/decide-v4: gone] Fix demo to handle variants
  haacked/fix-sample-app ec15751 [origin/haacked/fix-sample-app] Handle variants
* haacked/local-only     e78d2f6 Do important stuff
  main                   ab885d0 [origin/main] chore: Bump to v1.0.2

注意haacked/decide-v4被标记为gone。

我们可以使用git的porcelain命令以更易解析的格式列出分支及其跟踪信息:

1
2
3
4
5
6
> git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/

haacked/decide-v4 [gone]
haacked/fix-sample-app
haacked/local-only
master

让我们创建一个别名来列出这些gone分支:

1
> git config --global alias.gone "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '\$2 == \"[gone]\" { print \$1 }'"

现在我们可以使用这个别名来列出gone分支:

1
2
3
> git gone

haacked/decide-v4

下一步是更新我旧的bclean别名以使用新的gone别名:

1
> git config --global alias.bclean "!git gone | xargs -r git branch -D"

不幸的是,由于git不知道它已被合并,我们必须进行强制删除。这有点吓人,但这不会触及本地分支或任何仍在跟踪远程分支的分支。使用这个别名,你可以运行git bclean来删除所有已合并到默认分支的分支。最后,我们有旧的bdone别名来切换到默认分支,运行git up,然后运行bclean

以下是本文中提到的完整别名集合,你可以直接复制粘贴到你的.gitconfig中:

1
2
3
4
5
6
[alias]
    default = "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"
    bclean = "!git gone | xargs -r git branch -D"
    # 切换到指定分支(如果未指定分支则使用默认分支),运行git up,然后运行bclean
    bdone = "!f() { DEFAULT=$(git default); git checkout ${1-$DEFAULT} && git up && git bclean; }; f"
    gone = "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '$2 == \"[gone]\" { print $1 }'"

或者,如果你想使用git命令行,可以使用以下命令:

1
2
3
4
git config --global alias.default "!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"
git config --global alias.gone "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '\$2 == \"[gone]\" { print \$1 }'"
git config --global alias.bclean "!git gone | xargs -r git branch -D"
git config --global alias.bdone "!f() { DEFAULT=\$(git default); git checkout \${1:-\$DEFAULT} && git up && git bclean; }; f"

注意:使用git default别名时,你可能会遇到以下错误:

1
fatal: ref refs/remotes/origin/HEAD is not a symbolic ref

这个别名依赖于origin/HEAD符号引用的存在。在某些情况下,特别是新克隆的仓库或某些配置中,这个引用可能没有被设置。要修复它:

1
git remote set-head origin --auto

现在,我的旧工作流程又回来了。当我合并一个PR时,我可以从合并的分支运行git bdone来清理分支。世界又恢复正常了。

和往常一样,你可以在我的dotfiles仓库中找到这些别名和更多内容。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计