GitHub Stacked PR을 써보자
Stacked PR이 무엇인지, 왜 쓰는지, 어떻게 쓰는지에 대해 정리했다.
최근 GitHub Stacked PR이 추가됐다. 막연히 ‘오 좋겠네.’ 하고 넘어갔었는데 pr -> review 자동화 파이프라인을 테스트하던 도중에 문득 직접 써보고 싶어졌다. 사용법과 어떨때 유용할지, 엣지 시나리오에선 어떻게 동작하면 좋을지 등을 정리해봤다. 유용한 기능임은 분명하나 agent보단 사람의 판단이 들어가야 의미가있겠다는 생각이 들었다.
모든 시나리오는 내 상상속 시나리오를 기반으로 했다.
GitHub Stacked PR이란 무엇인가?
개발하면서 커밋은 작업단위로 신경써서 쪼개지만 막상 pr을 올릴때가 되면 범위가 너무 커져서 리뷰를 할때도, 받을때도 곤란한 상황이 있었을것이다. PR을 쪼개야한다. 1pr = 1branch니까 스스로 생각한 단위에 맞춰 Branch를 나누고, 그에맞는 commit을 넣어줘야한다.
# HEAD = feat/workspace, Target = develop
git checkout develop
git checkout -b feat/workspace-a
# 본인이 생각한 PR단위에 맞는 commit-hash들을 적용
git cherry-pick commit-hash
git push -u origin feat/workspace-a # base
git checkout -b feat/workspace-b # layer 1
# .. 반복
기억해야하는건 1개의 기능을 여러 PR로 쪼갠다는거다. 그렇기때문에 base가 될 브랜치가 존재하고, layer가 될 브랜치는 그 base로부터 딴다. 만약 target이 될 브랜치에서 각 layer를 딴다면 각 layer는 별개의 기능이라는 소리다.
한 시나리오를 생각해보자. 각 layer가 이전 layer에 의존성을 가지고있어 layer1 -> layer2 -> layer3의 흐름을 갖는데 layer2가 리뷰로 인해 수정됐다. 그럼 layer3은 새로이 rebase되어야 할것이다.
이렇게 1개의 기능을 여러 pr로 쪼갤때 생기는 불편한 지점들을 해결하려고 나온게 stacked pr이라고 생각하면 좋다.
사용법
GitHub PR UI에서도 가능하지만 agent 연동까지 생각해서 GitHub-CLI로 알아보자. cli에서는 별도의 extension을 설치해야한다.
gh extension install github/gh-stack
테스트를위해 bookmark를 등록하고 보여주는기능이 전부인 간단한 프로젝트가 있다고 가정하자. 여기에 웹서버, 그리고 데이터를 받아서 뿌려주는 List 컴포넌트를 개발했다. 최종 log는 아래와같이 찍혔다.
feat: bookmark-fe-ui # commit-hash = a1
feat: bookmark-fe-data # a2
feat: bookmark-be # a3
feat: bookmark-config # a4
init # a5 HEAD
이제 PR을 올리면 a1~a4 커밋이 한번에 존재할것이다. 리뷰할때 성격이다른 커밋들이 합쳐져서 보일테니 불편하다. 이걸 gh stack을 이용해서 각 pr로 쪼개보자.
지금은 테스트를위해 쪼개질 layer를 미리 염두에 두고 커밋까지 해놓았지만, 실제로는 pr단위를 스스로 생각하고 그에맞게 어떤 커밋을 넣을지 고민해봐야한다.
gh stack init feat/bookmark-config # switch to feat/bookmark-config branch
git cherry-pick a4
1pr = 1branch라고 했다. base layer가 될 브랜치를 실제로 생성해준 다음 해당브랜치에 커밋을 옮겨붙인게 전부다. 단 base가된 브랜치는 trunk에서 분기된다. pr은 결국 trunk에 merge되기때문에 그렇다.
GitHub의 default branch가 develop이라면 trunk도 develop이 되지만, --base, -b 플래그로 trunk가 될 branch를 바꿀 수도 있다. 다음 layer를 쌓아보자.
gh stack add feat/bookmark-be # switch to feat/bookmark-be branch
git cherry-pick a3
init은 말그대로 base를 선언해주는거고, 그다음부터는 add로 쌓는다. 여기까지하고 현재 stack상태를 살펴보자. 차곡차곡 쌓이고있는 아름다운 뷰까지 제공한다.
gh stack view

bookmark-fe-ui까지 쌓고 pr을 제출한 뒤 GitHub PR UI에서 확인해봤다. pr목록엔 4개의 pr이 올라와있고 우측엔 작은 stack아이콘이 보인다.
gh stack submit

가장 나중에쌓인 layer에 merge를 누르면 나머지 pr들은 자동 merge되니 주의해야한다. 별개의 pr이 아니라 layer마다 의존성이 있으니 당연한 동작이다. 각 pr마다 rebase, squash등 merge 전략을 선택할 수 있다.
Agent와 함께 사용해보자
사실 이모든 과정을 agent를 통해 딸깍이 가능하다. skill로서 gh-stack기능을 제공하는데 먼저 install해보자.
gh skill install github/gh-stack
사용하는 agent를 선택하면 알맞은 위치에 gh-stack이란 스킬이 생긴다. 내용은 에이전트가 GitHub stack을 이용할때 지켜야할 가이드라인이라고 생각하면 좋을것같다. layer분리 기준을 명시적으로 설명하면 그대로 나눠주지만, 그렇지않다면 agent한테 layer 분리까지 위임하는 내용도 있다.
하지만 stack에서 중요한건 layer를 어떤 기준으로 가르는지 판단하는것이다. 그래서 layer분리만큼은 스스로 정해야하지않을까? 싶다.
예상 시나리오
특정 layer에 코드리뷰 후 반영
feat/bookmark-be가 리뷰후 수정되어서 올라온 상황을 가정해보자. 위에쌓인 bookmark-fe-data는 rebase해서 새로쌓인 bookmark-be 위에 올려야한다.
그런데 rebase는 위에 쌓인 commit-hash를 전부 변경시키기때문에 fe-data 위에쌓인 fe-ui역시 rebase해야한다. 여기서 stack을 또 이용해보자.
git checkout feat/bookmark-be # 리뷰브랜치로 이동
git add . && git commit -m "리뷰 수정"
gh stack rebase --upstack # HEAD = feat/bookmark-be
gh stack push
bookmark-be 위로 쌓인 layer를 전부 rebase해주고 모두 push해주는거까지 간편하게 처리 가능하다.
특정 layer를 제거
feat/bookmark-be를 다음작업으로 미루고싶은 상황에선 modify를 이용해주면 된다. cli내에서 친절하게 설명되어있으니 인터랙티브하게 제거하면 된다.
gh stack modify
gh stack submit # 변경된 stack으로 submit
n개의 pr중에 m개는 merge된 상태
bookmark-be가 merge됐다고 가정하자. 하위 레이어인 bookmark-config까지 merge된 상태일 것이다. 즉 remote stack은 merge가 반영됐으니 local stack도 맞춰준다음에 stack을 만져주면 된다.
gh stack sync # sync up with remote stack
gh stack modify # or something
불편한점
- merge 되고나서도 local
.git의gh-stack은 초기화가 안된다.gh stack unstack --local로 한번에 비울 수 있지만 행여 잊는다면 추후 layer 브랜치를 만들때 이미 있다고 나올수도있다. remote에도없고 로컬에도없는데? 싶다면.git/gh-stack에 있을 확률이 높다. - merge시 자동 head브랜치삭제옵션이 꺼져있다면 layer브랜치가 전부 remote에 남아 지저분해지니 주의하도록 하자.