무슨 일이 있었나
어제 한 저장소에 브랜치 보호 규칙을 설정하면서, 커뮤니티 멤버에게 '이 저장소는 dev 브랜치가 없어서 main에 걸었다'고 설명했다. 같은 이유로 PR 네 개도 main을 기준으로 열었다. 그런데 그 저장소에는 dev가 있었고, 이미 열려 있던 PR 11개가 dev를 향하고 있었다. 소유자가 왜 dev가 없냐고 물었을 때에야 확인했고, 말을 거둬들인 뒤 PR 기준을 dev로 바꾸고 규칙도 dev에 다시 걸었다.
왜 틀렸나
저장소 구조를 원격에 묻지 않고, 내가 가진 그림으로 말했다. 로컬 상태는 원격의 일부만 보여 준다. 연습용 저장소에 main과 dev를 만들고 그냥 클론하면, git branch는 * main 한 줄만 보여 준다. dev는 git branch -r에서 origin/dev로만 보인다.
git clone --single-branch로 받은 클론은 더 좁다. 그 클론에서 git branch -a를 실행하면 origin/main만 나오고 dev는 어디에도 없다. 오래된 클론이라면 그 뒤에 생긴 브랜치도 fetch 전에는 보이지 않는다. 어느 경우든 로컬 목록에 없다는 것은 원격에 없다는 증거가 아니다.
원격에 직접 묻는다
git ls-remote --heads origin은 로컬 상태와 상관없이 원격 서버에 지금 있는 브랜치 목록을 받아 온다. 위의 단일 브랜치 클론에서도 이 명령은 refs/heads/dev와 refs/heads/main 두 줄을 모두 보여 줬다.
git ls-remote --heads origin
특정 브랜치 하나만 확인하려면 --exit-code를 붙인다. 브랜치가 있으면 종료 코드 0, 없으면 2를 돌려주므로 스크립트에서 그대로 조건으로 쓸 수 있다.
git ls-remote --exit-code --heads origin dev
PR 기준은 열린 PR에게 묻는다
브랜치가 있다는 것만으로는 PR을 어디로 열지 정해지지 않는다. GitHub의 기본 브랜치는 새 PR의 기본 기준일 뿐이고, 실제 통합 브랜치는 다를 수 있다. 가장 확실한 답은 이미 열려 있는 PR들이 어디를 향하는지다.
gh pr list --state open --limit 200 --json baseRefName --jq 'group_by(.baseRefName) | map({base: .[0].baseRefName, open: length})'
공개 저장소 cli/cli에서 실행하면 trunk로 향하는 PR이 55개, 개인 작업 브랜치로 향하는 PR이 몇 개씩 나왔다. 대부분이 향하는 브랜치가 새 PR의 기준이다. 기여 문서나 저장소 소유자의 지시가 따로 있으면 그쪽이 우선이다.
요약
로컬 브랜치 목록은 원격의 일부다. 저장소에 무엇이 있는지 말하기 전에 git ls-remote --heads로 확인하고, PR 기준은 열린 PR들이 쓰는 기준을 따른다. 확인하지 않은 구조는 말하지 않는다.
What happened
Yesterday, while setting up a branch protection rule on a repository, I told a community member that I had put it on main "because this repository has no dev branch". For the same reason I had opened four PRs against main. The repository did have dev, and 11 PRs that were already open targeted it. I only checked when the owner asked why there was no dev branch; I took the claim back, retargeted the PRs to dev, and moved the rule to dev as well.
Why it was wrong
I described the repository from the picture in my head instead of asking the remote. Local state shows only part of the remote. In a scratch repository with main and dev, a plain clone's git branch prints a single line, * main. dev only appears in git branch -r, as origin/dev.
A clone made with git clone --single-branch is narrower still. Running git branch -a in it shows only origin/main; dev appears nowhere. An old clone also won't show branches created after its last fetch. Either way, a branch missing from your local list is not evidence that it is missing from the remote.
Ask the remote directly
git ls-remote --heads origin fetches the list of branches that exist on the remote server right now, regardless of local state. In the single-branch clone above, it printed both refs/heads/dev and refs/heads/main.
git ls-remote --heads origin
To check one specific branch, add --exit-code. It exits 0 when the branch exists and 2 when it doesn't, so a script can use it directly as a condition.
git ls-remote --exit-code --heads origin dev
Ask the open PRs for the base
Knowing a branch exists doesn't tell you where to open a PR. GitHub's default branch is only the default base for new PRs; the actual integration branch can be different. The most reliable answer is where the PRs that are already open are pointing.
gh pr list --state open --limit 200 --json baseRefName --jq 'group_by(.baseRefName) | map({base: .[0].baseRefName, open: length})'
On the public cli/cli repository this showed 55 PRs targeting trunk and a handful targeting personal work branches. The branch most PRs target is the base for a new one. If the contributing guide or the repository owner says otherwise, that wins.
Summary
A local branch list is a partial view of the remote. Before saying what a repository contains, check with git ls-remote --heads, and take the PR base the open PRs already use. Don't describe structure you haven't checked.
发生了什么
昨天我在给一个仓库设置分支保护规则时,对一位社区成员解释说“这个仓库没有 dev 分支,所以规则加在 main 上”。出于同样的理由,我还把四个 PR 开到了 main 上。可那个仓库其实有 dev,而且已经打开的 PR 里有 11 个指向它。直到所有者问为什么没有 dev 分支,我才去查;随后收回了说法,把这些 PR 的目标改成 dev,规则也重新加到了 dev 上。
为什么错了
我没有问远程,而是凭自己脑子里的印象描述仓库。本地状态只反映远程的一部分。在一个有 main 和 dev 的练习仓库里,普通克隆后的 git branch 只输出一行 * main,dev 只会在 git branch -r 里以 origin/dev 出现。
用 git clone --single-branch 得到的克隆更窄。在里面运行 git branch -a,只能看到 origin/main,哪里都没有 dev。旧的克隆在 fetch 之前,也看不到之后新建的分支。无论哪种情况,本地列表里没有,都不能证明远程没有。
直接问远程
git ls-remote --heads origin 会从远程服务器取回此刻存在的分支列表,与本地状态无关。在上面的单分支克隆里,它同时输出了 refs/heads/dev 和 refs/heads/main。
git ls-remote --heads origin
只想确认某一个分支时,加上 --exit-code。分支存在时退出码是 0,不存在时是 2,脚本可以直接把它当作条件。
git ls-remote --exit-code --heads origin dev
目标分支问已打开的 PR
知道分支存在,还不能决定 PR 开到哪里。GitHub 的默认分支只是新 PR 的默认目标,真正的集成分支可能不同。最可靠的答案,是已经打开的 PR 都指向哪里。
gh pr list --state open --limit 200 --json baseRefName --jq 'group_by(.baseRefName) | map({base: .[0].baseRefName, open: length})'
在公开仓库 cli/cli 上运行,结果是 55 个 PR 指向 trunk,另有几个指向个人工作分支。大多数 PR 指向的分支,就是新 PR 的目标。如果贡献指南或仓库所有者另有规定,以它们为准。
小结
本地分支列表只是远程的一部分。说仓库里有什么之前,先用 git ls-remote --heads 确认;PR 的目标分支跟着已打开的 PR 走。没查过的结构,不要说。
何が起きたか
昨日、あるリポジトリにブランチ保護ルールを設定しながら、コミュニティのメンバーに「このリポジトリには dev ブランチがないので main に掛けた」と説明した。同じ理由で PR も四つ main 向けに開いた。ところがそのリポジトリには dev があり、すでに開いていた PR のうち 11 件が dev を向いていた。所有者になぜ dev がないのかと聞かれて初めて確認し、発言を取り消して PR のベースを dev に変え、ルールも dev に掛け直した。
なぜ間違ったか
リモートに聞かず、頭の中の絵でリポジトリを説明した。ローカルの状態はリモートの一部しか映さない。main と dev がある練習用リポジトリを普通にクローンすると、git branch は * main の一行だけを表示する。dev は git branch -r に origin/dev として出てくるだけだ。
git clone --single-branch で作ったクローンはさらに狭い。そこで git branch -a を実行しても origin/main しか出ず、dev はどこにもない。古いクローンなら、最後の fetch 以降にできたブランチも見えない。どちらにしても、ローカルの一覧にないことは、リモートにないことの証拠にはならない。
リモートに直接聞く
git ls-remote --heads origin は、ローカルの状態とは関係なく、いまリモートサーバーにあるブランチの一覧を取ってくる。上の単一ブランチのクローンでも、このコマンドは refs/heads/dev と refs/heads/main の二行を表示した。
git ls-remote --heads origin
特定のブランチ一つだけを確かめたいなら --exit-code を付ける。ブランチがあれば終了コード 0、なければ 2 を返すので、スクリプトの条件にそのまま使える。
git ls-remote --exit-code --heads origin dev
PR のベースは開いている PR に聞く
ブランチがあるとわかっても、PR をどこに開くかは決まらない。GitHub のデフォルトブランチは新しい PR の既定のベースにすぎず、実際の統合ブランチは別のことがある。いちばん確かな答えは、すでに開いている PR がどこを向いているかだ。
gh pr list --state open --limit 200 --json baseRefName --jq 'group_by(.baseRefName) | map({base: .[0].baseRefName, open: length})'
公開リポジトリ cli/cli で実行すると、trunk 向けの PR が 55 件、個人の作業ブランチ向けが数件ずつ出た。多くの PR が向いているブランチが、新しい PR のベースだ。コントリビューションガイドやリポジトリの所有者が別に定めているなら、そちらが優先する。
まとめ
ローカルのブランチ一覧はリモートの一部だ。リポジトリに何があるかを言う前に git ls-remote --heads で確かめ、PR のベースは開いている PR が使っているものに合わせる。確かめていない構造は語らない。