Reorient GitHub Pull Requests Around Changesets

Mitchell Hashimoto

GitHub Pull Request를 Changeset 중심으로 재구성하기

저는 1,000명 이상의 컨트리뷰터가 참여하는 초대형 오픈소스 프로젝트의 메인테이너로서, 초대형 폐쇄형 기업 프로젝트의 엔지니어로서, 그리고 그보다 작은 규모의 모든 프로젝트에서 GitHub를 사용해 왔습니다. 지금까지의 경험을 통틀어 보면, GitHub에서 보내는 시간의 거의 대부분을 pull request에서 보내고 있으며, 안타깝게도 제게는 GitHub에서 가장 답답한 부분이기도 합니다.

pull request에 바라는 개선점은 많지만, 제가 겪는 문제의 상당 부분은 단 하나의 큰 기능으로 해결될 수 있습니다. 바로 changeset입니다. 이 글에서는 이 제안을 구체적으로 설명하고 제가 바라는 모습을 이야기하려 합니다.

면책 조항: 제 아이디어는 독창적인 것이 아닙니다! 제가 이 아이디어를 고안했다고 주장하지 않습니다. 여기서 제안하는 내용은 이미 충분히 검증된 Git 워크플로에 기반한 것이며, Gerrit이나 Phabricator, 혹은 전통적인 이메일 기반 패치 리뷰 같은 다른 제품들에서 부분적으로 또는 완전히 구현되어 있기도 합니다.


오늘날의 문제

오늘날 GitHub pull request의 생명주기는 사실상 하나의 거대한 가변(mutable) changeset입니다. 이게 바로 혼란의 원인입니다!

요즘의 전형적인 PR을 살펴보겠습니다. 컨트리뷰터가 브랜치에 커밋 묶음을 푸시하고 PR을 열면, PR은 그 브랜치를 그대로 대표하게 됩니다. 사람들은 댓글로 PR에 대해 논의합니다. 컨트리뷰터가 새로운 변경 사항을 푸시하면 같은 PR에 바로 나타나 즉시 업데이트됩니다. 리뷰어가 댓글을 남기는 동시에 컨트리뷰터가 변경 사항을 푸시할 수도 있고, 이 모든 것이 동일한 PR을 업데이트합니다.

이 방식에는 여러 문제가 있습니다:

  • 리뷰어가 PR의 이전 상태를 기준으로 리뷰를 남겼는데, 리뷰를 작성하는 동안 컨트리뷰터가 변경 사항을 푸시해 리뷰가 즉시 유효하지 않게 될 수 있습니다.

  • 더 심각한 경우, 리뷰가 부분적으로만 유효하지 않게 되어 나머지 피드백이 컨트리뷰터가 푸시한 변경 사항의 맥락에서는 이해되지 않을 수 있습니다. 예를 들어 라인 코멘트에 “앞선 코멘트와 동일한 피드백입니다”라고 적혀 있는데, 컨트리뷰터가 해당 라인을 옮기는 변경을 푸시하는 바람에 이전 코멘트가 사라지거나 숨겨지는 경우가 있습니다.1

  • 리뷰에는 어떤 커밋에 대해 남겨졌는지에 대한 메타데이터가 없고 제출된 타임스탬프만 남습니다. 사용자는 타임스탬프를 커밋과 대략적으로 연결해 볼 수 있지만 완전히 정확하지는 않습니다. 리뷰 도중에 커밋이 들어오면 타임스탬프상으로는 가장 최근 커밋 이후에 작성된 것처럼 보이지만, 실제로는 이전 커밋을 보고 작성한 리뷰일 수 있기 때문입니다. 😕

  • 리뷰 피드백을 반영하는 작업 중인 커밋은 브랜치에 푸시되는 즉시 노출됩니다. 이 때문에 컨트리뷰터는 모든 피드백을 하나의 커밋으로 처리해야 하거나, 리뷰어가 부분적으로만 반영된 피드백을 감수해야 합니다.

  • PR의 이전 상태로 쉽게 돌아가 살펴볼 수 없습니다. 이후 커밋을 제외하고 이전 커밋 묶음만 리뷰하고 싶다면 “compare” 뷰를 직접 만들거나 로컬에서 체크아웃해야 합니다(저는 후자를 사용합니다). 하지만 어느 쪽을 쓰더라도 코드 변경 사항만 볼 수 있을 뿐, 그 시점에 남겨진 리뷰 피드백까지 함께 볼 수는 없습니다!

  • 위와 비슷하게, 컨트리뷰터가 여러 개의 새로운 커밋을 푸시하면 이전 커밋 묶음과 새로운 커밋 묶음을 쉽게 비교할 수 없습니다. 사실상 한 번에 하나의 커밋씩만 살펴볼 수 있습니다. 이 경우에도 결국 로컬 git으로 직접 diff를 만들어 비교해야 합니다.

  • 그 외에도 문제점이 더 있습니다... 뒤에서 몇 가지를 더 다루겠지만, 여기서 요점은 충분히 전달되었다고 생각합니다.

위에서 언급한 내용 중 일부 세부 사항은 제가 틀렸을 수도 있습니다. 누군가는 “5(a) 문제는 이렇게 하면 해결됐을 텐데”라고 말할지도 모릅니다. 그런 지적도 도움이 됩니다! 하지만 제가 말하고자 하는 요점은 한 걸음 물러서서 보면 이러한 문제들을 일으키는 근본 원인이 진짜 문제라는 점입니다. 바로 브랜치를 커밋 단위로 추적하는 단일 가변 changeset이라는 구조 자체입니다.


Changeset

해결책은 changeset입니다: pull request를 단조 증가하는 번호(v1, v2, ...)로 버전 관리할 수 있게 하는 것입니다. 이러한 버전을 보통 “changeset”이라고 부릅니다.

각 changeset은 특정 시점의 브랜치 상태를 가리킵니다. 이 버전들은 불변(immutable)합니다. 새로운 커밋이 푸시되면 새로운 changeset의 일부가 되고, 컨트리뷰터가 브랜치를 강제 푸시해도 역시 새로운 changeset이 됩니다. 이전 changeset은 그대로 영구 보존됩니다.

새 changeset은 즉시(커밋 단위로) 게시할 수도 있고, 컨트리뷰터가 리뷰를 위해 새 버전을 제안하기로 결정할 때까지 발행을 미룰 수도 있습니다. 후자의 방식에서는 컨트리뷰터가 이전 피드백을 반영해 여러 커밋을 만든 뒤, 준비가 되었다고 판단될 때만 변경 사항을 게시할 수 있습니다.

changeset 기반에서는 피드백이 특정 changeset에 귀속됩니다. 리뷰어가 어떤 changeset을 리뷰하는 중에 새로운 changeset이 게시되더라도, 리뷰 자체가 원자적 단위로 이전 changeset에 연결되어 있기 때문에 문제가 되지 않습니다.

이후 changeset에서는 이전 changeset에 아직 해결되지 않은 코멘트가 있는 파일이나 라인을 표시해 두는 것이 유용한 경우가 많습니다. 이렇게 하면 이전 changeset에 대한 피드백이 유실되지 않고, 어떤 changeset이 승인되기 전에 반드시 처리되도록 할 수 있습니다.

일반적으로 각 changeset은 서로 다른 Git ref로 표현됩니다. 예를 들어 오늘날 GitHub pull request는 보통 refs/pr/1234와 같은 형태로, 로컬에서 git을 이용해 어떤 pull request든 체크아웃할 수 있습니다. changeset이라면 가령 refs/pr/1234/v2(가상의 예시) 같은 형태가 되어 개별 changeset을 체크아웃할 수도 있게 됩니다.

PR 자체를 “승인”하고 병합하는 대신, 리뷰어는 changeset을 승인합니다. 이렇게 하면 컨트리뷰터가 하나의 PR 안에 동일한 문제에 대해 서로 다른 접근 방식을 담은 여러 changeset을 올릴 수도 있고, 메인테이너는 최신이 아닌 changeset을 병합할 변경 사항으로 선택할 수도 있습니다.


GitHub, 부탁드립니다!

changeset은 수많은 오픈소스 프로젝트와 기업에서 이미 잘 정립된 패턴입니다. Gerrit이나 Phabricator 같은 기존 제품들에서는 사용자 경험 측면에서도 이미 충분히 탐구된 문제이기도 합니다. 또한 changeset은 기존 동작을 깨지 않는 방식으로 도입할 수 있다고 생각합니다. 현재의 PR이 사실상 단일 가변 changeset 모드로 동작하고 있기 때문입니다.

changeset이 도입되면 pull request는 대규모 프로젝트와 조직에서 훨씬 더 확장성 있게 동작할 수 있습니다. 확장성뿐 아니라, pull request에 참여하는 양쪽 모두가 더 깔끔하고 안전하게 리뷰를 진행할 수 있게 됩니다.

물론 이는 어디까지나 저 개인과 제 경험에 한한 이야기이지만, 이 하나의 큰 기능만으로도 GitHub를 사용하는 동안 제 삶의 질과 작업 능력이 크게 개선될 것입니다.2

각주

  1. 사소하고 불편한 문제처럼 보이지만, 규모가 커지면 심각한 문제로 확대됩니다.

  2. “그냥 GitHub를 쓰지 않으면 되지!”라는 말을 들어본 적이 있습니다. 제가 오늘날 GitHub를 쓰는 데에는 여러 다른 이유가 있기 때문에, 개인적으로는 당장 현실적인 선택지가 아닙니다. GitHub를 쓰지 않아도 괜찮다면, 다른 제품에서 changeset 지원을 찾을 수 있습니다.

원문은 Mitchell Hashimoto님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.