Published on

GitButler 톺아보기

Authors
  • avatar
    Name
    윤종원
    Twitter

GitButler란?

PixelSnap 2024-02-18 at 14.59.18@2x.png

최근 슬랙에서 한 분이 새로운 Git 패러다임을 제시하고 있는 Git Client가 나왔다며 GitButler를 소개해주셨다. 첫 커밋 자체는 작년(2023) 1월부터이니 어떻게 보면 초기 프로젝트이고, 아직까지 많은 기능이 구현되어 있지는 않은 상태이다.

Star History Chart

다만 위 스타 히스토리를 보면 알 수 있듯이 최근에 많은 관심을 받고 있는 프로젝트이고, GitButler가 제시하는 패러다임이 흥미로워서 이 글에서 다루게 되었다. 이 글을 작성하는 2024년 2월 18일 기준 최신 버전이 0.10.10으로 매우 초기 버전이고, 빠르게 기능 개발이 이루어지고 있어 이 글을 읽는 시점에는 기능이나 UI가 많이 달라져있을 수도 있으니 참고 바란다.

또 다른 Git Client?

사실 세상에는 이미 수많은 Git Client들이 나와있다. 오래 전부터 소게되던 Source Tree부터 시작해서 GitKraken, Fork, Tower, GitHub Desktop 등 수많은 클라이언트들이 존재한다. 나 같은 경우에는 IDE와 다른 Client을 오가는 것이 싫어서 JetBrain의 IDE에 내장된 Git Client와 Fork를 함께 사용했고 필요한 경우에만 CLI를 사용했다. 이렇게만 해도 Git 자체를 사용하는 데는 큰 문제가 없었으나 동시에 여러 작업을 해야할 때, 다른 사람과 협업하면서 작업 컨텍스트를 자주 바꿔야만 할 때 이제는 당연해진 불편함이 있었다.

사실 GitButler의 CEO이자 co-founder인 Scott Chacon은 Git 쪽에서 무척 유명한 사람이다. Git을 공부하다 보면 한 번쯤 보았을 Git 페이지를 만들었으며 Pro Git의 저자이기도 하다. 그리고 Git Flow와 종종 비교되는 Github Workflow를 소개한 사람이기도 하다. Git에 대한 경험이 풍부하고 오랫동안 많은 사람들이 Git을 더 잘 이해하고 잘 사용할 수 있도록 노력해온 사람이 GitButler를 왜 만들게 되었는지 이제 궁금해지지 않는가? Git Buttler 문서의 'Why GitButler' 항목을 살펴보면 다음과 같은 내용이 있다.

Our goal is to make sure that nobody ever has to read Scott's book again. That you don't have to learn how to manage your source code management tool.

여기서 'Scott's book'은 위에서 말한 Pro Git 책을 말한다. 여러 Git Client들은 Git을 직관적으로, 그리고 쉽게 사용할 수 있도록 도와주지만 이는 Git 동작을 감싸줄 뿐이다. GitButler가 하고자 하는 것은 Git과의 상호작용을 새롭게 설계하는 것이다.

이렇게만 들으면 이런 의문이 들 수도 있다.

지금까지 Git 잘 사용하고 있었는데 뭐가 또 필요한데?

Git을 잘 사용하고 있던 사람들은 '당연해진 불편함'이 잘 와닿지 않을 수도 있고, Scott이 말한 Git의 어려움에 대해 잘 공감이 가지 않을 수도 있다. 또한 GitButler는 아직 초기 버전이라 그리 기능이 많지도 않다. 그래서 이 글에서는 우리가 일반적으로 현업에서 접하는 상황을 가정하고, GitButler를 사용하면 어떻게 편해질 수 있는지를 위주로 소개해보려 한다.

GitButler 설치하기

PixelSnap 2024-02-17 at 23.53.43@2x.png

2024년 2월 18일 기준 GitButler는 macOS와 Linux만 지원하고 있다. Window 지원에 대한 이슈는 여기에서 확인해볼 수 있는데 GitButler는 Tauri 기반으로 개발되었기에 빠른 시일 내에 Windows 지원이 될 것으로 보인다. 하지만 현재는 Window에서 사용할 수 없다.

위 링크에서 GitButler를 설치하고 실행하면 위와 같은 화면이 나온다.

기본적으로 로그인 없이 사용할 수 있지만 GitButler가 제공해주는 브랜치 이름 추천, 커밋 메시지 생성 등의 AI 기능을 사용하려면 로그인을 해야만 한다.

로그인을 하고 나면 위와 같은 화면이 나온다. 여기에서 Add New Project를 누르면 GitButler에서 로컬에서 관리하던 Git 프로젝트를 추가할 수 있다.

프로젝트를 추가하고 나면 Project에 대한 기본적인 설정을 할 수 잇다. GitButler에서는 Base Branch 혹은 Target Branch를 필요로 한다. 설정 화면에 잘 나와있는 것처럼 우리가 production이라고 생각하는 브랜치를 설정하면 된다. 브랜치에서 작업을 마쳤을 때 머지되는 브랜치라고 생각하면 된다.

GitButler features를 활성화하면 커밋 메시지 생성 등 OpenAI의 API를 이용한 기능을 사용할 수 있으며, Github features에서는 PR 생성 등 Github과 연동된 기능을 사용할 수 있다.

프로젝트를 연동하고 나면 위와 같이 gitbutler/integration 이라는 브랜치가 생성된다. 이는 Integration Branch로 자세한 내용은 문서를 확인하자. GitButler에서 Virtual Branch를 관리하기 위해 사용하는 전용 브랜치라고 생각하면 된다. 이 브랜치에 체크아웃했다는 것은 GitButler를 사용하겠다는 의미이며 이 브랜치에 있을 때는 다른 Git Client를 사용해서는 안 된다. 기존의 Git은 여러 브랜치를 한 번에 다룰 수 없기 때문에 예기치 못한 동작을 보일 수 있기 때문이다. 자세한 내용은 여기를 참고하자.

처음 프로젝트를 연동하면 위와 같은 화면이 된다. 왼쪽 사이드 바에서는 Trunk와 Workspace를 확인할 수 있으며 그 외에 기존의 Git 브랜치를 확인할 수 있다. 해당 기능들은 아래 사용 예시를 살펴보면서 다시 다루도록 하겠다.

사용 예시

간단한 작업

먼저 단순하게 하나의 브랜치를 파고, 작업을 하고, 커밋을 하고, PR을 올리는 과정을 살펴보자.

// Person.kt
class Person private constructor(val name: String) {
    companion object {
        fun create(): Person {
            return Person("Mark")
        }
    }
}

// main.kt
fun main() {
    val person = Person.create()
    println(person.name)
}

우선 간단하게 Person 클래스를 작성하고, main 함수에서 Person 클래스를 사용하는 코드를 작성했다. Person.kt 파일을 생성했고, main.kt 파일을 수정했다.

그러면 다른 Git Client처럼 변경된 파일 목록을 확인할 수 있다.

CleanShot 2024-02-18 at 15.50.08.gif

함께 커밋하고 싶은 파일들을 옆으로 드래그 해 손쉽게 브랜치를 생성할 수 있다. 기본적으로 브랜치 이름은 자동으로 만들어주지만 원한다면 직접 이름을 지정할 수도 있다. 커밋하고 싶은 파일들을 선택하고, 커밋 메시지를 직접 작성하거나 AI를 통해서 작성할 수 있다. 이렇게 만든 커밋들을 발로 Remote에 푸시할 수도 있고, PR을 올릴 수도 있다. 다만, 현재 GitButler의 문제로 클라이언트 상에서 PR 생성이 안 되는 문제가 있어서 이 글에서는 직접 Github에서 PR을 생성했다.

위처럼 GitButler를 사용해서 Remote에 푸시하면 우리가 평소에 Git을 사용하던 것처럼 브랜치를 만들고, 파일을 추가하고, 커밋 메시지를 작성하고, 푸시한 것처럼 보인다.

실제 GitHub에서 위 PR을 머지하고나면 GitButler에서는 어떻게 반영되는지 확인해보자. 기본적으로 GitButler에서는 Remote에 새로운 변경사항이 있는지 주기적으로 확인한다. 원한다면 Trunk 옆에 있는 새로고침 버튼을 눌러 직접 업데이트하는 것도 가능하다.

CleanShot 2024-02-18 at 16.17.46.gif

만약 Remote 상에서 새로운 변경 사항(예를 들어 PR이 머지되었을 때)이 있으면 GitButler에서는 이를 감지하고, 우리에게 알려준다. Workspace 옆에 새롭게 생긴 Update 버튼을 누르면 Remote 상의 변경사항을 우리의 Local에도 반영해준다. 이 과정에서 위가 새롭게 생성한 Virtual Branch가 머지되었으므로 자동으로 Virtual Branch도 삭제해준다.

간단하게 살펴본 기능을 요약하면 다음과 같다.

  • 파일들을 드래그하는 것만으로도 손쉽게 브랜치를 생성할 수 있다.
  • GitButler에서 작업한 내용을 커밋하고 Remote에 푸시하거나, PR을 만들 수 있다.
  • Remote 상에서의 변경 사항을 자동으로 확인하고, 이를 내 로컬에 편하게 반영할 수 있다.

여러 개의 작업을 동시에 진행할 때

이번에는 여러 개의 작업을 동시에 진행할 때 GitButler가 어떻게 도움을 줄 수 있는지 살펴보자. 기존에는 Person 객체를 생성하기 위해서 create 라는 메소드 이름을 사용하고 있었다. 누군가 해당 메소드 이름을 더 자연스럽게 born이라는 이름으로 변경하고 싶다고 해보자.

class Person private constructor(val name: String) {
    companion object {
        fun born(): Person {
            return Person("Mark")
        }
    }
}

위와 같이 변경할 수 있을 것이다. GitButler 상에서는 다음과 같이 표시된다.

이상적으로는 하나의 단일 작업을 끝냈으니 이에 대한 커밋을 작성해야겠지만 일하다 보면 커밋을 까먹거나, 여러 작업을 동시에 진행하게 되어서 커밋을 놓치는 경우가 종종 있다. Car라는 클래스를 추가하는 작업도 동시에 진행해보자.

data class Car(val model: String) 

그러면 GitButler 상에서는 다음과 같이 표시된다.

해당 작업을 하나의 커밋으로 묶거나, 커밋은 분리하고 하나의 브랜치에 올릴 수도 있다. 하지만 코드를 리뷰하는 입장에서는 왜 두 작업이 함께 묶여있는지, 혹시 두 작업이 서로 관련이 있는지 파악하려 할 것이다. 물론 PR을 올릴 때 이에 대한 설명을 적을 수는 있다. 하지만 PR을 작성할 때 추가적인 공수가 들고, 코드 리뷰어 입장에서도 이를 신경쓰며 코드를 봐야하니 불편하다. 결국 이상적인 방법은 각각 브랜치를 만들고, 각각의 작업을 커밋하고, 각각의 PR을 올리는 것이다. 아마 git이 익숙한 사람들은 다음과 같이 작업할 것이다.

git checkout -b refac-person-create-name
git add src/Person.kt src/main.kt
git commit -m "refactor: change create to born"
git push origin refac-person-create-name

git checkout -b add-car-class
git add src/Car.kt
git commit -m "feat: add Car class"
git push origin add-car-class

정말 간단한 작업인 경우에 위처럼 Git CLI를 사용해 작업할 수 있지만 그렇지 않은 경우 위와 같이 작업하는 게 불가능한 경우도 있다. 대부분의 사람들은 이러한 작업이 귀찮아 그냥 한 번에 올려버리고 PR에서 설명을 적는 경우가 많다. 이러한 경우에 GitButler는 어떻게 도움을 줄 수 있을까?

CleanShot 2024-02-18 at 16.34.37.gif

작업 컨텍스트를 전환하지 않고 손쉽게 각각에 대한 브랜치를 생성하고, 커밋하고, 푸시해 PR을 올리는 것이 가능하다. Car 클래스에서도 생성자를 만드는 작업을 추가로 한다고 해보자.

class Car private constructor(val model: String) {
    companion object {
        fun create(name: String): Car {
            return Car(name)
        }
    }
}

이렇게 추가적인 작업을 했을 때도 GitButler에서는 손쉽게 이를 기존 브랜치에 추가할 수 있다.

기존에 Car.kt 파일을 해당 브랜치에서 수정했기에 자동으로 들어가있지만, 기본적으로는 Default Branch에 수정한 파일이 추가된다. 만약 한동안 한 가상 브랜치에서 작업할 것이라면 Set as default 버튼을 눌러 이후 작업할 파일들이 해당 브랜치에 들어가도록 설정하는 것도 가능하다. 매우 편하게 작업 컨텍스트를 전환하고, 여러 브랜치를 오가면서 작업할 수 있을 것 같은 느낌이 든다.

Stacked Changes

개인적으로 GitButler를 살펴보면서 Stacked Changes와 비슷한 방식으로 작업할 수 있겠다는 느낌을 받았다. Stacked Changes에 대한 자세한 내용은 이 영상을 참고하자. 이 글에서는 상황을 가정하며 기존의 작업 방식이 왜 불편한지를 살펴보고, GitButler가 어떻게 이를 해결할 수 있는지 살펴보자.

class Person private constructor(val name: String) {
    companion object {
        fun born(): Person {
            return Person("Mark")
        }
    }
}

기존에는 Person 클래스를 생성할 때 이름이 고정되어 있었다. Person 클래스를 생성할 때마다 무작위의 이름을 부여하고 싶다고 해보자. 이를 구현하기 위해서는 무작위의 이름을 생성할 수 있는 알고리즘이 필요하고, Person 클래스가 이를 사용하도록 수정해야 한다.

fun generateRandomString(length: Int): String {
    val leftLimit = 'a'.code
    val rightLimit = 'z'.code
    val random = Random()
    return (1..length)
        .map { random.nextInt(rightLimit - leftLimit + 1) + leftLimit }
        .map { it.toChar() }
        .joinToString("")
}

위와 같이 'a'부터 'z'까지의 문자열을 무작위로 생성하는 함수를 만들었다. 다만 과연 Kotlin에서 이 방법이 최선인지 의문이 들어 이에 대한 코드 리뷰를 먼저 따로 받고 싶다고 해보자. 여기서는 Person 클래스만 이 함수를 사용할 테니까 굳이 그럴 필요가 있을까? 싶지만 이 메소드를 사용하려는 작업이 무척 큰 작업이라면 작게 PR을 쪼개서 받고 싶을 수도 있다. 이러한 불편함에 대해서는 Stacked Changes에서도 다루었던 내용이니 이를 참고하자.

class Person private constructor(val name: String) {
    companion object {
        fun born(): Person {
            return Person(generateRandomString(10))
        }
    }
}

결국 우리가 하고자 하는 것은 위와 같이 Person 클래스가 generatedRandomString 함수를 사용하도록 수정하는 것이다. 하지만 generatedRandomString에 대한 브랜치를 별도로 파고, 이에 대한 코드 리뷰를 받는 다면 일반적으로 코드 리뷰가 끝나고 해당 브랜치가 머지되기 전까지는 이 작업을 할 수가 없다. 로컬에서만 수정 사항을 따로 유지하거나, 커밋을 체리픽하거나, generatedRandomString 브랜치 위에 새로운 브랜치를 만드는 등의 복잡한 방법을 사용해야 한다. 혹은 generatedRandomString PR이 코드 리뷰가 끝나고 머지되기 전까지 Person 클래스를 수정하지 않고 기다려야 한다. 이러한 불편함을 GitButler가 어떻게 해결할 수 있는지 살펴보자.

우선, generatedRandomString 함수만 만들었을 때는 위와 같이 표시된다. 이에 대한 브랜치를 만들고, PR을 올려두었다고 해보자.

다음과 같은 상태가 되어있을 것이다. 아래와 같이 PR도 만들고, 코드 리뷰를 요청했다고 해보자.

코드 리뷰도 요청했으니 이제 우리는 Person 클래스를 수정하고 싶다. generatedRandomString 함수는 다른 브랜치에 있는데 새로운 브랜치에서 어떻게 이 함수를 사용할 수 있을까? GitButler에서는 이러한 고민을 할 필요가 없다. 모든 작업은 가상 브랜칭 나뉘어있을뿐 실제로는 하나의 브랜치에서 작업하기 때문에 generatedRandomString 함수를 사용할 수 있다. 이를 통해 우리는 Person 클래스를 수정하고, 이에 대한 커밋을 만들고, PR을 올릴 수 있다.

class Person private constructor(val name: String) {
    companion object {
        fun born(): Person {
            return Person(generateRandomString(10))
        }
    }
}

위와 같은 수정을 하고 나면 다음과 같은 상황이 된다.

우리의 로컬에는 generatedRandomString에 대한 작업 내용이 여전히 존재하며 이러한 작업들은 가상 브랜치를 통해서 나뉘어있을 뿐이다. 덕분에 generatedRandomString 브랜치가 머지되기 전에 미리 작업하고, 커밋해두는 것이 가능하다.

따라서 우리는 한 작업이 다른 작업에 의존하는 상황에서도 각각에 대한 PR을 만들고, 코드 리뷰를 받고, 머지되기 전까지 다른 작업을 할 수 있다.

사실 generatedRandomString 함수는 다음과 같이 작성할 수도 있다.

fun generateRandomString(length: Int): String {
    val allowedChars = ('A'..'Z') + ('a'..'z')
    return (1..length)
        .map { allowedChars.random() }
        .joinToString("")
}

이러한 부분을 코드 리뷰 상에서 지적 받고, 이를 수정해 브랜치를 머지했다고 해보자.

Trunk를 업데이트하면 generateRandomString 함수에 대한 브랜치가 Integrated로 표시되는 것을 확인할 수 있으며 Update 버튼을 눌르면 해당 가상 브랜치가 삭제된다.

지금까지 살펴본 내용에 대해 정리하면 다음과 같다.

  • 가상 브랜치를 통해 손쉽게 여러 작업을 동시에 진행하고, 각각에 대한 PR을 만들고, 코드 리뷰를 받을 수 있다.
  • 다른 작업에 의존하는 작업을 할 때도 다른 작업의 코드 리뷰를 기다리지 않고도 편하게 작업할 수 있다.

기타

사실 GitButler에는 Commit Amend, Squash, Apply 등 이 글에서 다루지 않은 기능들도 많다. 기존 브랜치를 가상 브랜치로 전환하거나 Merge Cnflict에 대한 부분 또한 이 글에서는 다루고 있지는 않다. 이에 대해서는 GitButler의 유튜브 채널이나 문서를 확인하는 것을 추천한다.

결론

사실 GitButler에는 아직 부족한 기능도 많고 버그도 적지 않게 존재한다. 하지만 GitButler가 제시하는 Butler Flow라는 새로운 패러다임이 꽤나 흥미로웠고 Graphite와 다르게 쉽게 사용할 수 있을 것 같은 느낌으 받아 짧게나마 이 글에서 다루어봤다.

이 글은 전적으로 GitButler를 보고 내가 어떻게 사용할 수 있을지, 어떻게 편해질 수 있을지를 다룬 글이다. 따라서 개개인의 작업 방식이나 팀의 구조에 따라서 공감되는 부분이 있을 수도 있고, 그렇지 않은 부분이 있을 수도 있다고 생각한다. 이 글에서 놓친 부분이 있거나 다르게 생각하는 부분이 있다면 댓글로 달아주면 이 글을 읽는 모두가 더 Git을 잘 사용할 수 있게 되지 않을까 싶다.