refspec 是 git fetch 和 git push 的核心语法:冒号两边是什么、加号是什么、星号怎么配对。这篇用一个本机的裸仓库当远程,逐个验证这些写法的行为,包括被拒绝的和强制覆盖的情形。
$ 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/*。
$ 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 取全部。
$ 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, usepush 同样是 refspec:HEAD:refs/heads/新名字 创建分支,HEAD:refs/review/42 创建任意命名空间的 ref;不带参数的 git push 受 push.default 约束,simple 要求分支名与上游相同。