bigywater.tech
Git · 33

upstream 与 ahead/behind 的计算

git status 里的 “ahead 2, behind 3” 是怎么算出来的?upstream 到底存在哪里?@{u} 和 @{push} 有什么不同?这篇造出一个分叉的场景,把这些全部拆开验证。

2 和 3
领先 2、落后 3 的分叉:git rev-list --left-right --count main...origin/main 输出 2 3
2 项配置
upstream 就是 branch.<名>.remote 和 branch.<名>.merge
gone
远程分支被删并 fetch --prune 之后,git branch -vv 显示 [origin/topic: gone]

upstream 是什么

实测 · 本机执行输出
  $ git config --get-regexp "^branch\." ; git branch -vv | sed -E 's/ [0-9a-f]{7} / <sha> /'
  branch.main.remote origin
  branch.main.merge refs/heads/main
  * main <sha> [origin/main] m1

分支的上游只是 config 里的两项:branch.<名>.remote 和 branch.<名>.merge。git branch -vv 把对应的本地缓存 origin/main 和领先落后数显示出来。

领先与落后

实测 · 本机执行输出
  $ git status -sb | head -1; echo "(fetch 之前:不知道远程多了 3 个)"
  ## main...origin/main [ahead 2]
  (fetch 之前:不知道远程多了 3 个)
  $ git fetch -q; git status -sb | head -1; git status | sed -n 2,3p; git branch -vv | sed -E 's/ [0-9a-f]{7} / <sha> /'
  ## main...origin/main [ahead 2, behind 3]
  Your branch and 'origin/main' have diverged,
  and have 2 and 3 different commits each, respectively.
  * main <sha> [origin/main: ahead 2, behind 3] A 本地 2

ahead = 本地分支有而上游没有的提交数(upstream..branch),behind = 上游有而本地没有的提交数(branch..upstream)。两个数都不是 0 就是 diverged。

只和缓存比

实测 · 本机执行输出
  $ git log --oneline origin/main..main | sed -E "s/^[0-9a-f]+ //"; echo "-- 以上是领先的提交(只在本地)"; git log --oneline main..origin/main | sed -E "s/^[0-9a-f]+ //"; echo "-- 以上是落后的提交(只在远程)"
  A 本地 2
  A 本地 1
  -- 以上是领先的提交(只在本地)
  B 远程 3
  B 远程 2
  B 远程 1
  -- 以上是落后的提交(只在远程)
  $ git rev-list --left-right --count main...origin/main; echo "(左边 = main 独有,右边 = origin/main 独有)"; git merge-base main origin/main | cut -c1-7 | sed "s/.*/merge-base = <sha>/"; git log -1 --format=%s $(git merge-base main origin/main)
  2	3
  (左边 = main 独有,右边 = origin/main 独有)
  merge-base = <sha>
  m1
  不同的写法等价:
  $ git rev-list --count origin/main..main; git rev-list --count main..origin/main; git rev-list --count ^origin/main main; git rev-list --count main --not origin/main
  2
  3
  2
  2

数字用的是本地缓存的 origin/main,所以在 fetch 之前看不到远程新增的提交。

一次数两个

实测 · 本机执行输出
  $ git rev-parse --abbrev-ref @{u}; git rev-parse --abbrev-ref @{upstream}; git rev-parse --abbrev-ref main@{u}; git rev-parse --abbrev-ref @{push}
  origin/main
  origin/main
  origin/main
  origin/main
  $ git remote add fork /work/l29remote.git; git fetch -q fork; git config remote.pushDefault fork; git rev-parse --abbrev-ref @{push}; echo "-- 换成 push.default=current:"; git -c push.default=current rev-parse --abbrev-ref @{push}; echo "-- @{u} 不受影响:"; git rev-parse --abbrev-ref @{u}
  -- 换成 push.default=current:
  fork/main
  -- @{u} 不受影响:
  origin/main
  fatal: cannot resolve 'simple' push to a single destination
  $ git config --unset remote.pushDefault; git config branch.main.pushRemote fork; git -c push.default=current rev-parse --abbrev-ref @{push}; git config --unset branch.main.pushRemote; git remote remove fork
  fork/main

git rev-list --left-right --count main...origin/main 给出 左边独有、右边独有 两个数;origin/main..main、^origin/main main、main --not origin/main 是等价的写法。

@{u} 与 @{push}

实测 · 本机执行输出
  $ git switch -q -c plain; git config --get branch.plain.merge || echo "plain: 没有 upstream"
  plain: 没有 upstream
  $ git switch -q -c from-main main; git config --get branch.from-main.merge || echo "from-main(起点是本地分支): 没有 upstream"
  from-main(起点是本地分支): 没有 upstream
  $ git switch -q -c topic origin/topic 2>&1; git config --get branch.topic.remote; git config --get branch.topic.merge
  origin
  refs/heads/topic
  $ git switch -q plain; git push -q -u origin plain 2>&1; git config --get branch.plain.remote; git config --get branch.plain.merge
  origin
  refs/heads/plain
  $ git switch -q from-main; git branch -q --set-upstream-to=origin/main; git rev-parse --abbrev-ref from-main@{u}
  origin/main
  $ git config --get branch.autoSetupMerge || echo "branch.autoSetupMerge 未设置(默认 true:只有起点是 remote-tracking 分支才设置)"
  branch.autoSetupMerge 未设置(默认 true:只有起点是 remote-tracking 分支才设置)

@{u} 是 fetch / pull 的来源,@{push} 是 push 的去向,可以不同(例如 fork 工作流里从 origin 拉、往 fork 推)。push.default=simple 要求二者在同一个 remote,否则 @{push} 解析失败。

自动设置

实测 · 本机执行输出
  $ git switch -q topic; git fetch -q --prune; git branch -vv | grep topic | sed -E 's/ [0-9a-f]{7} / <sha> /'
  * topic     <sha> [origin/topic: gone] B 远程 3
  $ git status | sed -n 1,3p
  On branch topic
  Your branch is based on 'origin/topic', but the upstream is gone.
    (use "git branch --unset-upstream" to fixup)

只有新分支的起点是 remote-tracking 分支(git switch -c topic origin/topic)才会自动设置 upstream(branch.autoSetupMerge 默认 true);其它情况要 push -u 或 --set-upstream-to。上游被删之后显示 gone。

分步演示

upstream 与 ahead/behind
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。A、B 是同一个本机裸仓库的两个克隆;输出里的 hash 做了替换;@{push} 那一节里 fatal 行因标准错误排在输出末尾而出现在命令之后(它属于该节第一个 @{push})
    • 没有验证:branch.autoSetupMerge 的 always / simple / inherit 取值;--track=inherit;ahead/behind 在 shallow clone 或 partial clone 下的计算;status.aheadBehind=false(跳过计算);推送时 push.autoSetupRemote
    京ICP备2026067502号-1京公网安备11010502063487号