接手一个有年份的仓库时,真正让人头疼的往往不是代码写得烂,而是分支多到你不知道该看哪条。我在工作中见过不止一次这样的场面:一个项目的本地分支二十多个,名字里有 feature、fix、test、release、backup、old、final,后缀还有 2、3,甚至 final_final。想了解项目最近到底在动什么,第一反应是把分支拉出来,但 git 默认列表按字母排序,你看到的是 alex/test、bob/feature、carol/bugfix 这种排列,跟最近进展完全没关系。问上一个人哪个分支还在维护,对方大概率会回答“都别删,可能还有用”。所以这个需求听起来特别小——把分支按最后一次提交时间排个序——但实际操作之后你会发现,它背后真正解决的是一个分支治理问题。看懂排序,你才可能继续做清理;会清理,你才算真正掌控了这个仓库。
1. 分支排序这个需求,本质是仓库可见性问题
从用户视角看,git branch 的输出默认按字母排序。这是 Git 为了可预测性做的最朴素选择,但也是让人最困惑的地方。一个昨天还在合入需求的活跃分支,和三个月前就没人碰过的僵尸分支,在默认列表里并肩排着,看起来都差不多。真正的问题不在排序算法,而在“可见性”。一个仓库的信息如果只按字母组织,相当于一个项目文档只按文件名排,完全看不出哪些已经失效、哪些还在更新。
给分支按最后提交时间排序,本质上是在做一次“仓库体检”,让丢失的时间信息重新出现在列表里。这也是为什么我不建议把这个需求简单理解成“记住一条命令就好”。命令本身确实十秒就能学会,但如果不知道排序后要看什么、哪些分支能删、哪些分支其实被 worktree 占着,这条命令也就只能用来截图。
1.1 字母排序只告诉你名字,不告诉你状态
在一堆分支名里,你很容易把自己正在写的分支排在名字前面,或者把历史遗留的分支放在中间。你想知道的是“最近到底哪个分支在被推进”,而不是“master 和 feature 谁排前面”。这就好比一个书架上的书全部按书名拼音摆,但你想找的是最近读过的、还能继续读的几本,位置信息就没用了。按时间重新排,本质上是一种按“活跃度”组织的视图。
1.2 分支本质上是一个指向提交的引用
再往下说,为什么 Git 能做这件事?因为分支在 Git 内部就是一个 ref,一个指向提交对象的命名引用。分支名本身不含时间信息,但被它指着的 commit 里有 committer date。排序就是把每个分支对应的 commit 时间取出来,作为排序键。
理解这一点,很多奇怪现象就能解释了:比如 rebase 之后分支时间会变,因为 commit 被重写了;比如一个分支虽然一直存在,但没人动它,它的时间就永远停留在最后一次提交。分支的时间不是分支被创建的时间。许多读者会把 Git 里的时间等同于“创建时间”,但 Git 在普通命令里并不直接暴露“分支创建时间”这个属性,最接近来源是 reflog,所以用 commit 时间排序反而是最稳定、也最符合直觉的方案。
2. 两条命令:git branch 和 for-each-ref 怎么用
排序在编程里是入门级功能,但 Git 里的 sort 不处理普通数组,处理的是 refs 元数据,所以两条命令都围绕“引用”展开。
2.1 最常用的一条:git branch --sort=-committerdate
先记住最基础、也最实用的命令。
git branch --sort=-committerdate这条命令列出本地分支,按 committer date 倒序排列,最新提交的分支在最上面。默认只列本地分支,不加参数时最直观。
如果想看更完整的信息,加一个-v参数。
git branch -v --sort=-committerdate-v会显示每个分支当前指向的 commit 短哈希和提交标题。输出结果类似:
* main 1a2b3c4 Merge pull request #102 feature/report 5e6f7a8 Add export report old/experiment 9f8e7d6 Try old approach这时你已经能快速判断:哪些分支是最近在动的,哪些已经很久没人碰了。
如果希望旧分支在最上面,去掉-前缀即可。
git branch --sort=committerdate2.2 想要完整信息:git for-each-ref才是更灵活的选择
git branch适合日常快速看,但一旦要做脚本处理、自定义字段、机器可读输出,我更推荐git for-each-ref。它更底层,也更稳定。
git for-each-ref --sort=-committerdate refs/heads/ \ --format='%(committerdate:relative)%09%(refname:short)%09%(subject)'输出结果类似:
3 days ago feature/report Add export report 2 weeks ago main Merge pull request #102 6 months ago old/experiment Try old approach这里每个字段的含义是:
refs/heads/:限定只看本地分支。如果把这一段换成refs/remotes/或具体远程名,就能看远程跟踪分支。%(committerdate:relative):相对时间,比如 “3 days ago”。%09:制表符,用来分隔列。%(refname:short):分支短名,不显示refs/heads/前缀。%(subject):这条分支 tip 提交的标题。
如果想看到更具体的日期,可以用%(committerdate:short),输出是2024-05-12这种格式。想要完整时间,用%(committerdate:iso8601)。想要机器可读,用%(committerdate:unix),输出秒级时间戳。
一个很常用的组合:
git for-each-ref --sort=-committerdate refs/heads/ \ --format='%(committerdate:short) %(refname:short) %(subject)'这条输出简洁、稳定,适合直接放进截图或脚本里。
2.3 参数对照表:看懂--sort和输出字段
| 参数 | 作用 | 示例 |
|---|---|---|
--sort=-committerdate | 按最后提交时间倒序 | 最新分支排最上面 |
--sort=committerdate | 按最后提交时间正序 | 旧分支排最上面 |
-v | 给 git branch 增加哈希和标题 | 适合快速浏览 |
-r | 只列远程跟踪分支 | 查看远端情况 |
-a | 本地 + 远程分支 | 全局视图 |
--format=... | 自定义输出字段 | 机器可读或个性化展示 |
需要注意,git branch --format在较新版本里可用;如果遇到老版本不支持,或者想写脚本,直接用git for-each-ref --format=...更稳。
3. 为什么排序要用 committerdate,而不是 authordate
很多人在查资料时会看到committerdate和authordate两个字段,第一反应是“两个不都差不多吗”。实际差很多。
3.1 author date 是代码诞生的时间,committer date 是代码落库的时间
Git 的 commit 对象里有两组时间:
- author date:写这段提交的人最初标记的时间,通常代表“代码是什么时候写出来的”。
- committer date:这个 commit 对象是什么时候被创建或应用进当前分支的时间。
大多数情况下,这两者相同。但一旦经历 rebase、cherry-pick、amend,author date 会保留原提交的时间,而 committer date 会更新为操作时间。
举个例子:你三个月前写了一个 commit,今天把它 cherry-pick 到一个新分支上。新分支的提交对象里,author date 还是三个月前,committer date 是今天。
所以,要判断“这个分支最近有没有被维护”,更贴切的字段是committerdate。很多新人用authordate排序,发现 rebase 过的分支时间非常乱,就是因为把“作者什么时候写的”和“代码什么时候落到这个分支上”搞混了。
3.2 相对时间显示不影响排序,排序用的是时间戳
另一个常见误解是:%(committerdate:relative)显示成 “3 days ago”,会不会影响排序顺序?
不会。--sort在排序时读的是 commit 里的内部时间值,不会受显示格式影响。相对时间只是输出层的格式化,底层仍然按时间戳计算。
3.3--sort前面的-是什么意思
--sort=-committerdate里的-表示降序。这个约定在git for-each-ref和git tag等命令里都适用。如果去掉-,就是升序。
另外,--sort不仅能用于committerdate。比如:
git for-each-ref --sort=-creatordate refs/tags/这个可以用来排 tag。只要理解“refs + 时间字段”这个组合,分支、标签、远程跟踪分支都可以用同一套思路。
4. 排序之后,怎么把“旧分支”安全清理掉
排序本身是只读操作,看完之后自然有人会想:这个分支半年没动了,能不能删?
能,但要按顺序来。
4.1 第一步:先同步远程分支删除状态
如果远程已经有同事删掉了一些分支,你本地可能还留着对应的远程跟踪分支。不先同步,排序列表里会出现“幽灵分支”。
git fetch --prune--prune会在 fetch 时把本地已不存在的远程跟踪分支删掉。这是做分支治理前的基础操作。
4.2 第二步:用合并状态和提交时间筛出候选分支
只看提交时间还不够,还要看分支是否已合并到主干。
git branch --merged列出已合并到当前 HEAD 的分支。
git branch --no-merged列出未合并的分支。
如果只想找“最近 12 个月没有更新的本地分支”,可以这样:
cutoff=$(date -d '12 months ago' +%s 2>/dev/null || date -v-12m +%s) git for-each-ref --sort=committerdate \ --format='%(committerdate:unix)%09%(refname:short)%09%(subject)' refs/heads | awk -F '\t' -v c="$cutoff" '$1 < c {print $2 "\t" $3}'这整段是只读操作,不产生任何删除。输出的是超过 12 个月未更新的本地分支列表,后面是提交标题。脚本化时很有用。
4.3 第三步:删除前必须做的几件事
删除分支不是问题,问题是删错了恢复麻烦。我一般会按这个顺序处理:
- 先确认当前分支:
git branch --show-current。 - 对已合并分支,用
git branch -d <name>。Git 会做保护,只有分支的提交已经合并到上游或当前 HEAD,才允许删除。如果 Git 拒绝,说明还有未合并内容,要小心。 - 对确实不需要但未合并的分支,才用
git branch -D <name>强制删除。删除前务必再确认一遍提交时间和内容。 - 如果要清理远程分支,用
git push origin --delete <name>,而不是只删本地。 - 删除前最好备份分支引用:
git branch --no-color > branch-backup.txt,或者对整个仓库做一次git bundle create repo.bundle --all。
可以用这个判断表:
| 分支状态 | 判断 | 建议 |
|---|---|---|
| 已合并到主干 | 即使时间很久,也可以安全清理 | git branch -d |
| 未合并,但超过一年没更新 | 大概率已不需要 | 先看提交内容,再手动-D |
| 最近还在更新 | 活跃分支 | 保留 |
| 被 worktree 占用 | 直接删会失败 | 先处理 worktree |
| 只存在于本地、远程没有 | 可能是本地专属分支 | 优先确认 |
别一上来就写一个“自动删除超过三个月分支”的脚本。先跑只读命令,把候选列表打出来,人工看一遍,再删。自动化的下一步才是脚本,第一步永远是只读可视化。
5. 使用 worktree 时,分支排序和删除多了一层限制
很多团队在使用git worktree时,会突然发现:为什么我明明删不掉这个分支,报错信息却指向另一个目录?
这是因为 worktree 本质上是“同一个仓库的多份工作目录”。一个仓库可以有一个主 worktree,也可以额外增加多个 worktree,每个 worktree 都对应一个 working directory,并且通常会 checkout 一个具体分支。
这两者的区别可以一句话概括:
git branch管理的是“引用”,是提交图上的指针。git worktree管理的是“工作目录与 HEAD 的映射”,解决的是“同一个仓库能不能同时打开多个目录”。
5.1 一个分支只能被一个 worktree 检出
Worktree 的关键限制是:一个分支同一时间只能被一个 worktree checkout。如果你在主目录中检出了main,又在另一个目录里检出了feature/report,就不能在第三个目录再检出feature/report。
这个限制会直接影响删除操作。
5.2 删除被 worktree 占用的分支会发生什么
假设排序后发现某个分支超过半年没动,你想强制删除它,但它在另一个 worktree 里正被使用。Git 会直接拒绝:
error: Cannot delete branch 'feature/report' checked out at '/path/to/other-worktree'此时如果你用脚本批量删除,就会中断在这里。
所以,排序之后如果要清理,先执行git worktree list:
git worktree list输出会列出每个 worktree 对应的路径、HEAD 和分支。如果某个分支被 worktree 占用,你会在这里看得很清楚。
5.3 把 worktree 检查加进清理流程
如果想在脚本里自动判断哪些分支被 worktree 占用,可以用:
git worktree list --porcelain | grep '^branch refs/heads/' | sed 's#^branch refs/heads/##'这条命令会输出所有当前被 worktree 检出的本地分支名。用这张“占用名单”和“旧分支候选名单”做差集,就能避免误删。
在 worktree 模式下,分支列表的“活跃度”排序仍然有意义,但“能否删除”多了两道限制:合并状态,以及 worktree 占用状态。
不要以为
git branch -D是最强命令,它强不强制,都删不掉一个正被 worktree 使用的分支。Git 对“正在使用的引用”有保护机制,这不是 bug,是安全设计。
6. 把排序、过滤、清理固化成一周一次的小脚本
命令本身不复杂,但每次打开终端重新敲一遍,很容易漏掉某个步骤。我建议把“只读体检”固化成一个小脚本,每周跑一次,或者每接手一个新仓库时跑一次。
6.1 只读版:先列出去年未更新的本地分支
下面是一个可参考的 shell 脚本结构,核心是只读输出,不删除任何东西。
#!/usr/bin/env bash set -euo pipefail # Linux 用 date -d,macOS 用 date -v-12m cutoff=$(date -d '12 months ago' +%s 2>/dev/null || date -v-12m +%s) echo "== 最近 12 个月未更新的本地分支 ==" git for-each-ref --sort=committerdate \ --format='%(committerdate:unix)%09%(refname:short)%09%(subject)' refs/heads | awk -F '\t' -v c="$cutoff" '$1 < c {print $2 "\t" $3}'这段脚本会按时间正序列出旧分支,越久没更新的排越前面。注意,它不会自动删任何分支。
6.2 把合并检查、worktree 占用检查都加进去
只列出旧分支还不够。我通常会在脚本里再输出两张表:已合并分支列表、worktree 占用分支列表。
echo "" echo "== 已合并到当前分支的分支 ==" git branch --merged echo "" echo "== 当前 worktree 占用分支 ==" git worktree list --porcelain | grep '^branch ' || true这样一次脚本跑完,你会拿到三类信息:
- 哪些分支很久没更新。
- 哪些分支已经合并,可以安全用
git branch -d清理。 - 哪些分支正被 worktree 占用,删除前要绕开。
6.3 跨平台注意:date 命令在 Linux 和 macOS 上不一样
上面脚本用了双重写法,date -d在 Linux 上是标准参数,但在 macOS 上会报错,需要换成date -v-12m。脚本里的||就是用来做这种兼容的。
如果是 Windows 环境,用 Git Bash 执行基本没问题;如果直接在 PowerShell 里跑,建议把所有逻辑改写成 PowerShell 的Get-Date和git for-each-ref,或者干脆在 Git Bash 里跑。核心逻辑不变,只是语法差异。
7. 排序结果不对时,按这个顺序排查
遇到排序结果和直觉不一致,先别急着怀疑 Git,大多数情况是字段用错、范围看错、环境参数有问题。
7.1 先看现象
先确认具体是哪一种问题:
- 分支没排出来,列表是空的。
- 顺序是反的。
- 时间和自己预期不一致。
- 命令直接报错。
不同现象对应不同原因,不要一上来改命令。
7.2 再看范围
默认情况下,git branch对本地分支操作。如果你加了-r,就是远程跟踪分支。加了-a,才是本地加远程。
很多人排序后发现“远程分支好像过期了”,其实是因为没有先 fetch。远程跟踪分支里的 commit 时间只反映你上次 fetch 时的状态,不是实时状态。于是又回到那个建议:先git fetch --prune,再做排序和清理。
7.3 再看字段
排序结果和预期不一致,最常出现在这里。
- 用了
authordate而不是committerdate,导致 rebase 过的分支时间看起来混乱。 - 把字段拼错,比如写成
commitdate,Git 会报未知字段。 --sort=-committerdate的-被漏掉,结果顺序完全相反。
对脚本来说,字段拼写是最容易出问题的环节。建议先跑一条最小命令,确认字段名正确,再叠加 format。
7.4 最后看环境
- 在 Windows CMD 里用双引号包 format,有时会被解析得很奇怪。建议在 Git Bash 里执行,format 用单引号。
- 输出被分页器挡住,看不到完整列表,加
--no-pager或git --no-pager branch ...。 - 带了颜色后,管道处理可能受影响。脚本里用
--no-color或配置color.branch=false。
排查排序问题,用“现象、范围、字段、环境”四层就够了。先确认排的是哪一批分支,再确认用的哪个时间字段,然后看 git 版本和系统环境。
8. 别只收藏命令,把排序当成分支治理的第一步
回到开头说的那个场景:二十几个分支,名字混乱,谁也不敢删。如果你只是收藏了一条git branch --sort=-committerdate,它能帮你看到最近的动态,但解决不了全部问题。
真正有价值的动作,是把这条路走通:
- 跑只读排序列表,先用时间把分支分成“活跃”和“不活跃”两组。
- 对不活跃分支再看合并状态和 worktree 占用情况。
- 能安全删的用
git branch -d,不能确定的另存一份分支列表再决定。
这三步做完,分支数量会明显下降,留在列表里的都是真正有信息量的名字。
排序命令只是入口,它背后是一套分支生命周期管理思路:先看清,再筛选,最后才删除。把这一步沉淀成日常习惯,比记住任何一条命令都更有长期价值。