Git分支按提交时间排序:从命令到分支治理的完整指南 接手一个有年份的仓库时真正让人头疼的往往不是代码写得烂而是分支多到你不知道该看哪条。我在工作中见过不止一次这样的场面一个项目的本地分支二十多个名字里有 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 --sortcommitterdate2.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按最后提交时间倒序最新分支排最上面--sortcommitterdate按最后提交时间正序旧分支排最上面-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、amendauthor 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 --sortcommitterdate \ --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 listgit 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 -dmacOS 用 date -v-12m cutoff$(date -d 12 months ago %s 2/dev/null || date -v-12m %s) echo 最近 12 个月未更新的本地分支 git for-each-ref --sortcommitterdate \ --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 过的分支时间看起来混乱。把字段拼错比如写成commitdateGit 会报未知字段。--sort-committerdate的-被漏掉结果顺序完全相反。对脚本来说字段拼写是最容易出问题的环节。建议先跑一条最小命令确认字段名正确再叠加 format。7.4 最后看环境在 Windows CMD 里用双引号包 format有时会被解析得很奇怪。建议在 Git Bash 里执行format 用单引号。输出被分页器挡住看不到完整列表加--no-pager或git --no-pager branch ...。带了颜色后管道处理可能受影响。脚本里用--no-color或配置color.branchfalse。排查排序问题用“现象、范围、字段、环境”四层就够了。先确认排的是哪一批分支再确认用的哪个时间字段然后看 git 版本和系统环境。8. 别只收藏命令把排序当成分支治理的第一步回到开头说的那个场景二十几个分支名字混乱谁也不敢删。如果你只是收藏了一条git branch --sort-committerdate它能帮你看到最近的动态但解决不了全部问题。真正有价值的动作是把这条路走通跑只读排序列表先用时间把分支分成“活跃”和“不活跃”两组。对不活跃分支再看合并状态和 worktree 占用情况。能安全删的用git branch -d不能确定的另存一份分支列表再决定。这三步做完分支数量会明显下降留在列表里的都是真正有信息量的名字。排序命令只是入口它背后是一套分支生命周期管理思路先看清再筛选最后才删除。把这一步沉淀成日常习惯比记住任何一条命令都更有长期价值。