bigywater.tech
Git · 31

refspec 的语法与语义

refspec 是 git fetch 和 git push 的核心语法:冒号两边是什么、加号是什么、星号怎么配对。这篇用一个本机的裸仓库当远程,逐个验证这些写法的行为,包括被拒绝的和强制覆盖的情形。

+
refspec 开头的加号:去掉它,非快进更新会被 rejected;有它则 forced update
^
负 refspec 排除匹配的 ref(本例排除 refs/heads/feature/*)
HEAD:refs/review/42
把当前提交推到远程的任意命名空间,不限于 refs/heads

默认 refspec

实测 · 本机执行输出
  $ git ls-remote origin | sed -E 's/^([0-9a-f]{7})[0-9a-f]{33}/\1/'
  f461704	HEAD
  333bd73	refs/heads/dev
  e835792	refs/heads/feature/a
  76ebfb8	refs/heads/feature/b
  f461704	refs/heads/main
  e835792	refs/pull/1/head
  f461704	refs/tags/v1
  6728504	refs/tags/v2
  f461704	refs/tags/v2^{}
  $ git config --get-all remote.origin.fetch; git config remote.origin.url | sed "s#.*/#…/#"
  +refs/heads/*:refs/remotes/origin/*
  …/l27remote.git
  $ git fetch origin 2>&1 | sed -E "s/[0-9a-f]{7}\.\.[0-9a-f]{7}/<旧>..<新>/; s#From .*#From <远程>#"
  From <远程>
   * [new branch]      dev        -> origin/dev
   * [new branch]      feature/a  -> origin/feature/a
   * [new branch]      feature/b  -> origin/feature/b
   * [new branch]      main       -> origin/main
   * [new tag]         v1         -> v1
   * [new tag]         v2         -> v2
  fetch 之后本地的 refs:remotes/origin/HEAD  remotes/origin/dev  remotes/origin/feature/a  remotes/origin/feature/b  remotes/origin/main  tags/v1  tags/v2
  (没有 refs/pull/1/head:默认 refspec 只匹配 refs/heads/*;标签是 fetch 的“自动跟随”带来的)

git remote add 写下 +refs/heads/*:refs/remotes/origin/*:把远程的所有分支映射成本地的 origin/*,开头的 + 允许强制更新(远程分支被强推后,origin/* 仍能跟上)。refs/pull/ 这样的非分支命名空间默认不取。

语法

实测 · 本机执行输出
  $ git fetch -q origin 'refs/heads/feature/*:refs/remotes/o/f/*' 2>&1; git for-each-ref --format='%(refname)' | sed 's#^refs/##'
  remotes/o/f/a
  remotes/o/f/b
  remotes/origin/feature/a
  remotes/origin/feature/b
  tags/v1
  tags/v2
  $ git fetch -q origin 'refs/heads/feature/*:refs/remotes/o/x' 2>&1 | head -2
  fatal: invalid refspec 'refs/heads/feature/*:refs/remotes/o/x'
  $ git fetch -q origin 'refs/pull/1/head:refs/mine/pr1' && git for-each-ref refs/mine --format='%(refname)'
  refs/mine/pr1

[+]<src>:<dst>。src 是远程的 ref,dst 是本地的 ref;通配符 * 在两边各出现一次,否则报 invalid refspec。目标可以是任意命名空间。

快进与强制

实测 · 本机执行输出
  $ git fetch -q origin main:refs/heads/copy && git branch -v | sed -E "s/ [0-9a-f]{7} / <sha> /"
    copy <sha> m2
  $ git fetch origin main:refs/heads/copy 2>&1 | sed -E "s#From .*#From <远程>#; s/[0-9a-f]{7}\.\.[0-9a-f]{7}/<旧>..<新>/"
  From <远程>
   ! [rejected] main       -> copy  (non-fast-forward)
  $ git fetch origin +main:refs/heads/copy 2>&1 | sed -E "s#From .*#From <远程>#; s/[0-9a-f]{7}\.\.\.?[0-9a-f]{7}/<旧>...<新>/"
  From <远程>
   + <旧>...<新> main       -> copy  (forced update)

没有 + 时 fetch 到本地已有的 ref 只允许快进,否则 ! [rejected] … (non-fast-forward);加 + 则 forced update。

空的一边

实测 · 本机执行输出
  $ git push -q origin copy:refs/heads/tmp1 2>&1; git ls-remote origin refs/heads/tmp1 | sed -E 's/^[0-9a-f]{40}/<sha>/'
  <sha>	refs/heads/tmp1
  $ git push origin :refs/heads/tmp1 2>&1 | sed -E 's#To .*#To <远程>#'; git ls-remote origin refs/heads/tmp1 | wc -l; echo '(上一行是删除之后 ls-remote 数出的行数)'
  To <远程>
   - [deleted]         tmp1
         0
  (上一行是删除之后 ls-remote 数出的行数)
  $ git fetch origin dev 2>&1 | sed -E "s#From .*#From <远程>#"; cat .git/FETCH_HEAD | cut -f1,3 | sed -E "s#^([0-9a-f]{7})[0-9a-f]{33}#\1#; s# of .*##"; git for-each-ref --format="%(refname)" | sed "s#^refs/##"
  From <远程>
   * branch            dev        -> FETCH_HEAD
   * [new branch]      dev        -> origin/dev
  333bd73	branch 'dev'
  remotes/origin/dev
  只写 git fetch origin dev(没有 :dst)时,git 仍然顺手更新了 refs/remotes/origin/dev(机会性更新)

push 时 src 为空(:refs/heads/x)表示删除远程的 ref;fetch 时省略 dst 只写 FETCH_HEAD,但 git 会顺手更新能被配置的 refspec 映射到的 origin/*。

负 refspec 与叠加

实测 · 本机执行输出
  $ git config --add remote.origin.fetch '^refs/heads/feature/*'; git config --get-all remote.origin.fetch; git fetch -q origin 2>&1; git for-each-ref --format='%(refname)' | sed 's#^refs/##' | tr '\n' ' '; echo
  +refs/heads/*:refs/remotes/origin/*
  ^refs/heads/feature/*
  remotes/origin/HEAD remotes/origin/dev remotes/origin/main tags/v1 tags/v2
  $ git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pr/*'; git fetch -q origin 2>&1; git for-each-ref refs/remotes --format='%(refname)' | sed 's#^refs/##' | tr '\n' ' '; echo
  remotes/origin/HEAD remotes/origin/dev remotes/origin/main remotes/origin/pr/1

^ 开头的 refspec 排除匹配的 ref;多个 refspec 叠加,一个 remote 可以同时跟踪多个命名空间,比如 +refs/pull/*/head:refs/remotes/origin/pr/*。

标签

实测 · 本机执行输出
  git fetch            (只取 main)→ 本地标签: v1 v2
  git fetch --no-tags  (只取 main)→ 本地标签: (无)
  git fetch --tags     (只取 main)→ 本地标签: v1 v2
  git fetch 'refs/tags/*:refs/tags/*' → 本地标签: v1 v2

默认情况下,取回的提交上指向它的标签会被自动跟随;--no-tags 关闭,--tags 取全部。

push 的 refspec

实测 · 本机执行输出
  $ git push -q origin HEAD:refs/heads/from-local 2>&1; git push -q origin HEAD:refs/review/42 2>&1; git ls-remote origin 'refs/heads/from-local' 'refs/review/*' | sed -E 's/^[0-9a-f]{40}/<sha>/'
  <sha>	refs/heads/from-local
  <sha>	refs/review/42
  $ git config --get push.default || echo '(push.default 未设置,默认 simple)'; git push origin 2>&1 | head -4
  (push.default 未设置,默认 simple)
  fatal: The upstream branch of your current branch does not match
  the name of your current branch.  To push to the upstream branch
  on the remote, use

push 同样是 refspec:HEAD:refs/heads/新名字 创建分支,HEAD:refs/review/42 创建任意命名空间的 ref;不带参数的 git push 受 push.default 约束,simple 要求分支名与上游相同。

分步演示

refspec
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。远程是本机的一个裸仓库(file 路径),没有走网络;输出里的 hash、From 路径做了简化;第六节的“自动跟随”只验证了指向被取回提交的标签
    • 没有验证:fetch 的 --prune 与 refspec 的关系;push 的 --force-with-lease(见后面 push 一篇);remote..tagopt 配置;refspec 里的 glob 以外的模式;远程通过 ssh / http 时 refspec 的行为是否相同;push.default 的其它取值(matching、current、upstream)
    京ICP备2026067502号-1京公网安备11010502063487号