공개 저장소 agent-skill-garden에 브랜치 정리 스킬을 올리며 정리했다. 브랜치 수에 따른 비용은 합성 저장소에서 직접 쟀고 재현 코드도 함께 적었다. 손실 측정 예시(5,972 대 43)는 공개 저장소의 references 문서에 실린 값이다.

TL;DR

  • 브랜치는 쌓여도 당장 아프지 않다. 그래서 계속 미룬다.
  • 공유 저장소에서는 모두의 fetch·clone 비용으로 나타난다. 내용이 같은데 브랜치만 2,001개면 ref 협상 데이터가 103바이트에서 140,996바이트로, clone이 4배로 늘었다.
  • 로컬에서는 잃으면 안 되는 작업이 묻히는 문제로 나타난다. 2,001개 중 미푸시 커밋을 들고 있던 건 3개였는데, 목록만 봐서는 구분할 방법이 없다.
  • 미루면 정리 자체가 무서워진다. “이거 지우면 뭘 잃지”에 답하는 흔한 명령이 5,972개라고 답했다. 실제로는 43개였다.
  • 이 판단들을 스킬 3종으로 굳혀서 공개 저장소에 올렸다. 이제 “브랜치 정리해줘” 한마디면 된다.

브랜치는 쌓여도 아프지 않다

정리를 안 하게 되는 이유는 간단하다. 안 해도 아무 일도 안 일어나기 때문이다.

브랜치 하나는 41바이트짜리 파일 한 줄이다. 100개가 있든 2,000개가 있든 빌드는 그대로 돌고 테스트도 그대로 통과한다. 디스크도 안 찬다. 커밋 객체는 어차피 공유되니까.

그래서 “언젠가 정리해야지”가 몇 년씩 간다. 비용이 없어서가 아니라 비용이 나한테 청구되지 않아서다. 어디로 가는지 재봤다.


공유 저장소: 비용이 전원에게 분산된다

내용이 완전히 같고 브랜치 수만 다른 저장소 두 개를 만들어 비교했다. 커밋은 한 개, 파일도 한 개다.

base=$(git rev-parse HEAD)

# 브랜치 2,000개를 ref로만 생성 (커밋 객체는 하나를 공유한다)
for i in $(seq 1 2000); do
  echo "create refs/heads/feature/topic-$i $base"
done | git update-ref --stdin
  브랜치 1개 브랜치 2,001개
ref 광고 데이터 103 bytes 140,996 bytes
git clone 54 ms 226 ms

ref 광고는 clone이나 fetch를 할 때마다 서버가 “내가 가진 ref는 이것들이다”라고 먼저 보내는 목록이다. 브랜치가 늘면 이게 그대로 커진다. 위는 로컬 디스크 간 측정이라 순수한 ref 오버헤드만 잡힌 것이고, 실제로는 네트워크를 타므로 더 나빠진다.

중요한 건 이 비용을 브랜치를 안 지운 사람이 내지 않는다는 점이다. 저장소를 clone하고 fetch하는 모든 사람이 매번 나눠 낸다. CI도 포함이다. 파이프라인이 하루에 수백 번 fetch한다면 그만큼 반복된다.

숫자보다 실제로 더 거슬리는 건 따로 있다. 브랜치 선택 UI에서 원하는 걸 못 찾고, 자동완성이 쓸모없어지고, 어떤 게 살아있는 작업인지 아무도 모른다. 그래서 같은 일을 두 번 하거나, 죽은 브랜치를 base로 잡는 일이 생긴다.


로컬: 진짜 잃으면 안 되는 게 묻힌다

로컬은 성능 문제가 아니다. 신호가 소음에 묻히는 문제다.

같은 저장소에서 2,001개 중 딱 3개에만 푸시하지 않은 커밋을 심어놓고 봤다.

git branch --list 출력:  2,001줄
그중 미푸시 작업 보유:      3개

git branch의 출력은 2,001줄이 전부 똑같이 생겼다. 어느 3개가 위험한지 알 방법이 없다. 이게 로컬 브랜치가 쌓였을 때의 진짜 손해다. 저장소가 느려지는 게 아니라, 내가 잃으면 안 되는 것을 못 찾게 된다.

그리고 이 상태에서 “오래된 거 지우자”를 하면 정확히 그 3개를 지운다. 나이와 위험도는 상관이 없기 때문이다. 오히려 오래 방치된 브랜치일수록 푸시를 안 했을 확률이 높다.

제대로 분류하면 3개가 나온다. 대신 2,001개를 전부 대조해야 해서 73초가 걸렸다. 손으로 할 일이 아니다.

여기서 함정이 하나 더 있었다. 처음 짠 분류 스크립트는 위험 브랜치를 0개로 보고했다. 업스트림이 설정되지 않은 브랜치를 건너뛰었기 때문인데, 하필 그 3개가 전부 거기 있었다. 업스트림이 없다는 건 안전하다는 뜻이 아니라 비교 대상이 없다는 뜻이다.


미루면 정리가 무서워진다

오래 방치할수록 “이거 지워도 되나”에 답하기 어려워진다. 원격 브랜치를 지우기 전에 잃을 커밋 수를 세려고 가장 흔한 명령을 썼다.

git rev-list --count "$base".."$branch"

5,972개. 겁이 나서 diff도 재봤더니 6,713개 파일이 바뀐 것으로 나왔다. 이 숫자를 보면 지울 수가 없다.

둘 다 틀렸다. git cherry로 재면 43개다.

방법 결과 맞나
git rev-list --count 5,972 커밋 ❌ base를 브랜치로 머지해 들어온 커밋까지 셈
git diff --stat 6,713 files ❌ base가 그 뒤로 나아간 변경까지 셈
git cherry 43 커밋

세 명령 모두 정상 동작했다. 다른 질문에 답했을 뿐이다. 앞의 둘은 “이 두 ref가 얼마나 떨어져 있나”를 잰다. 내가 물은 건 “무슨 내용이 사라지나”였다. 오래 살아 있던 브랜치는 base를 여러 번 머지해 들여왔을 텐데, 그 커밋들은 base에 이미 있으니 잃을 게 아니다.

git cherry는 커밋을 patch ID로 비교한다. +는 base에 내용이 없는 커밋, -는 이미 반영된 것이다. squash 머지로 SHA가 완전히 달라진 커밋도 -로 잡는다. 5,972와 43의 차이는 정확도가 아니라 질문의 차이였다.


그래서 쉽게 만들었다

기준이 정해지고 나면 나머지는 반복 작업이다. 매번 다시 판단할 이유가 없어서 스킬 3종으로 굳혔다.

스킬 하는 일 원격에 쓰기
git-sync-shared-branches 공유 브랜치를 원격 기준으로 맞춤 없음
git-local-branch-cleanup 안 쓰는 로컬 브랜치 삭제 없음
git-remote-branch-cleanup 머지되거나 닫힌 내 원격 브랜치 삭제 삭제만

쓰는 법은 그냥 말하면 된다.

로컬 브랜치 정리해줘
공유 브랜치 최신화
원격 브랜치 정리해줘

권장 순서는 동기화 → 로컬 → 원격이다. origin/main이 낡아 있으면 “머지됐는지” 판정이 통째로 틀리기 때문에 동기화가 먼저다. 그다음 되돌릴 수 있는 로컬을 정리하고, 되돌리기 가장 어려운 원격을 마지막에 한다.

셋 다 스스로 결론내지 않는다. 후보와 근거를 내놓고 승인한 것만 실행한다. 수백 개일 때는 목록을 쏟지 않고 분포부터 보여준 뒤 기준을 조정할지 묻는다.

총 2001 개 · 현재 브랜치 제외
  1개월 이내(보존)    120
  1~3개월             340
  3~6개월             520
  6개월 초과         1020

삭제 후보:               1877 개
미푸시 보유, 손 안 댐:      3 개

원격 삭제는 reflog 같은 안전망이 없어서 기본이 dry-run이고, 지우기 전에 복구용 revision을 파일로 남긴다.

만들면서 구조를 한 번 갈랐다. 브랜치 관리는 일반적인 문제고 git은 실행 수단일 뿐이라, 판단은 SKILL.md에 두고 명령은 references/로 내렸다. 본문에는 git 명령이 한 줄도 없다. 호스트나 팀 규칙이 달라도 판단은 그대로 쓰인다.


배운 것

1. 비용이 분산되면 아무도 안 낸다. 브랜치를 남겨둔 사람은 아무것도 지불하지 않고, clone하고 fetch하는 전원이 조금씩 나눠 낸다. 이런 종류는 누군가 정리를 시작하기 전까지 계속 늘어난다.

2. 로컬 브랜치의 손해는 성능이 아니라 가시성이다. 2,001줄짜리 목록에서 위험한 3개를 찾을 방법이 없다는 게 핵심이다. 그리고 이 상태에서 나이로 자르면 정확히 그 3개가 날아간다.

3. 나이는 위험도와 상관이 없다. 오히려 오래 방치된 브랜치일수록 푸시가 안 돼 있을 확률이 높다. 지울 기준은 나이가 아니라 “지금 이걸 지우면 무엇이 사라지는가”다.

4. 숫자가 나왔다는 사실이 그 숫자가 맞다는 뜻은 아니다. rev-listdiff도 정상 동작했고 정확한 값을 냈다. 다만 내 질문에 답한 게 아니었다. 도구를 의심하기 전에, 이 명령이 답하는 질문이 내 질문과 같은지 먼저 볼 일이다.

5. 기준이 서면 반복은 넘긴다. 어려운 건 절차가 아니라 “지워도 되나”라는 판단이었다. 그 판단을 글로 굳혀두니 실행은 말 한마디가 됐다. 스킬을 53개 만들어놓고 3개만 쓰던 시절과 달라진 점이 있다면, 이번엔 함정을 먼저 겪고 그걸 규칙으로 적었다는 것이다.


브랜치 2,000개를 지우는 일 자체는 별 게 아니다. 미뤄지는 이유가 게으름이 아니라 “지워도 되는지 모르겠다”였다는 게 이번에 분명해졌다. 그 질문에 답할 방법을 정해두면 정리는 그냥 따라온다.

my-cursor에서 옮겨온 저장소에 이 스킬들을 올려뒀다. 브랜치가 쌓여 있다면 가져다 쓰면 된다.