--force 는 위험하니, git의 --force-with-lease 를 사용해보자

이라운·2023년 6월 24일
1
post-thumbnail

📰 이번에 다룬 뉴스:

Atlassian developer 소속 Steve Smith 의
–force considered harmful; understanding git’s –force-with-lease
✅ 작성자 편한대로, 이해한 대로, 기억하고 싶은 부분만 chat-gpt 와 함께 번역했습니다. 믿지 마시고, 되도록이면 위의 원문을 봐주세요.
⚡️ 해당 이모지가 있는 부분은 원문에 없는 작성자의 사족입니다.

✒️ 느낀 점

회사에서 git 을 사용하다가 때때로 머지를 잘못했다던지 하는 실수를 할 때가 있다. 여기서 나만 쓰는 feat 브랜치에서는 별생각 없이 --force 를 사용하지만, develop 브랜치의 내용을 머지하거나 변경이 필요해졌을 때 손이 달달 떨리는 불안감이 생겨나 안전하게 하는 방법을 찾다가 발견한 아티클이다. 이젠 개인 레포에서 사용할 때도 습관적으로 --force-with-lease 를 사용해야겠다.

🔤 번역

Git의 --force 는 무조건 적으로 원격 레포의 내용을 로컬의 내용과 상관없이 강제적으로 덮어씌워 팀원이 당시 푸시해놓은 모든 변화까지 덮어씌울 수 있는 매우 파괴적인 명령어이다. 하지만, 이를 대신할 더 나은 방법이 있다. 옵션 --force-with-lease 를 사용하는 것이다. 이것은 강제적으로 푸시를 할 수 있지만, 다른 이의 작업을 덮어씌우지 않도록 보장해주는 옵션이다.

git의 push --force 은 공유 레포에 이미 푸시된 다른 커밋을 망가뜨릴 수 있기 때문에 강력히 권장되지 않는 방법이라는 것은 매우 잘 알려져있다. 언제나 치명적인 것은 아니지만(다른 이의 워킹 트리에 변화가 있는 상태라면 머지를 통해 적용될 수 있다), 최소한적으로도 팀원을 고려하지 않는 방법이며 최악으로는 재앙적인 결정일 수 있다. 왜냐하면 --force 옵션은 내 개인 히스토리의 브랜치를 head 로 삼게만들며, 나의 로컬 브랜치와 평행적으로 이루어지던 모든 변화를 무시하기 때문이다.

강제적으로 푸시를 하게 만드는 원인 중에 하나는 브랜치를 리베이스 해랴 될 때이다. 간단한 예시를 들어보겠다.

어느 한 프로젝트에 앨리스와 밥이 같이 개발하고 있는 feat 브랜치가 있다고 가정해보자. 각자 이 레포를 클론해와서 작업을 시작할 것이다.

앨리스는 초기에 그녀의 기능을 개발했고 main 브랜치에 성공적으로 푸시를 했다. 여기까지는 문제없이 잘 이루어졌다.

밥도 자신의 일을 끝내고 푸시를 하려는데, master 브랜치에 변경이 있었다는 것을 알게됐다. 그는 깔끔한 트리를 만들고자, master 브랜치를 기준으로 리베이스를 수행하게 된다. 이렇게 되면 그가 리베이스된 브랜치를 푸시하려 할 때 당연하게도 머지가 거부될 것이다. 하지만 그는 앨리스가 이미 푸시를 했다는 것을 알지 못한 채로 push --force 를 하게되고, 결과적으로 앨리스의 모든 변경사항을 없애게 된다.

이 예시에서의 문제점은 밥이 강제 푸시를 할때 왜 그의 변경사항이 거부되었는지를 알지 못했다는 점이다. 그래서 그는 거부된 이유가 앨리스의 변경사항이 아닌 리베이스 때문일 것이라고 가정했다. 바로 이런 이유 때문에 공유되는 브랜치로의 --force 는 하지 말아야 할 행동으로 분류되는 것이다.

하지만 --force 는 덜 알려진 유사한 옵션을 가지고 있다. --force-with-lease 가 그것이다. 이 옵션은 파괴적은 강제 푸시 업데이트에 대해 일부를 보호할 수 있도록 한다.

--force-with-lease 는 브랜치에 지금까지 그 누구도 변화를 준 적이 없는지 확인하고, 변화가 있다면 강제 푸시를 못하도록 거부한다. 실제로는 원격 참조를 확인함으로써 가능하다. 원격 참조는 해시로 구성되어 있고, 그 값을 통해 부모 체인을 인코딩한다.

--force-with-lease 는 정확이 어떤 것을 참조할지도 지정할 수 있지만 기본적으로 현재의 원격 참조를 확인한다. 앨리스가 브랜치를 업데이트하고 원격 저장소에 푸시할 때 브랜치의 head 를 가리키는 참조가 업데이트 된다. 이제, 밥이 저장소를 pull 받지 않는 이상, 업데이트된 원격 참조에 대한 정보는 로컬에서는 존재하지 않게 된다. 따라서 --force-with-lease 를 통해 푸시를 하게되면, 로컬의 참조와 원격의 참조를 비교하여 강제 푸시를 허용하지 않게 된다. 즉 효과적으로 다른 이의 변경이 원격 참조에 반영이 안된 상태일 때에만 강제 푸시를 가능하도록 해준다. 안전 벨트가 있는 버전의 --force 라고 할 수 있다.

아래는 간단한 데모이다.

앨리스는 브랜치에서 작업을 수행했고 main 레포에 푸시까지 진행했다. 하지만 밥은 여기서 master 를 기준으로 리베이스를 진행했다.

bob$ git rebase master

First, rewinding head to replay your work on top of it...
Applying: Dev commit #1
Applying: Dev commit #2
Applying: Dev commit #3

리베이스가 된 상태로 푸시를 하려했지만, 서버는 앨리스의 작업을 덮어씌울 수 있기 때문에 거부한다.

bob$ git push

To /tmp/repo
 ! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to '/tmp/repo'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

밥은 이것이 리베이스 때문에 일어난 일이라고 생각하고 강제 푸시를 진행한다.

bob$ git push --force

To /tmp/repo
 + f82f59e...c27aff1 dev -> dev (forced update)

하지만 만약 그가 --force-with-lease 를 사용했더라면 다른 결과를 얻었을 것이다. 왜냐하면 git 이 원격 브랜치가 밥이 fetch 한 이후 새로이 업데이트된 부분이 있는지 확인하기 때문이다.

bob$ git push -n --force-with-lease

To /tmp/repo
 ! [rejected]        dev -> dev (stale info)
error: failed to push some refs to '/tmp/repo'

물론 git 을 사용할 때에 몇 가지 주의사항이 있다. 일반적인 것은 앨리스가 원격 레포에 그녀의 변경사항을 푸시했을 때만 동작한다는 것이다. 하지만 이것은 큰 문제가 아닌게, 그녀가 리베이스된 브랜치를 pull 받아오려고 할때 변경사항을 병합하도록 안내받게 되기 때문에 선택적으로 작업을 이에 따라 리베이스 할 수 있기 때문이다.

더 미묘한 문제는 브랜치가 수정되었음에도 git을 속여서 수정되지 않은 것으로 인식시킬 수도 있다는 것이다. 일반적인 사용법으로는 문제가 발생하지 않지만, 만약 밥이 로컬을 업데이트 하기 위해서 git pull 대신 git fetch 를 사용했을 때 발생한다. fetch 는 원격 참조와 객체를 가져오지만 merge 이 없으므로 작업 트리를 업데이트하지 않는다. 이로 인해 작업트리가 실제로는 새로운 작업을 포함하지 않도고 원격의 작업 복사본이 최신 상태인 것처럼 보이게 만들며, --force-with-lease 를 속여 원격 브랜치를 덮어씌우게 만들 수 있다.

bob$ git push -n --force-with-lease

To /tmp/repo
 ! [rejected]        dev -> dev (stale info)
error: failed to push some refs to '/tmp/repo'

bob$ git fetch

remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /tmp/repo
   1a3a03f..d7cda55  dev        -> origin/dev
   
bob$ git push --force-with-lease

Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (9/9), 845 bytes | 0 bytes/s, done.
Total 9 (delta 0), reused 0 (delta 0)
To /tmp/repo
   d7cda55..b57fc84  dev -> dev

이를 위한 가장 간단한 해결책은 "머지 없이 fetch 만 하지 말것" 이라고 하거나 "pull" 을 사용하라고 하는 것이지만, 만약 --force-with-lease 를 하기전에 fetch를 해야만 하느 ㄴ상황이라면 안전하게 할 수 있는 다른 방법이 있다. git 의 수많은 다른 기능과 마찬가지로 참조는 객체에 대한 임의의 포인터일 뿐이다. 따라서 우리는 참조를 직접 생성할 수 있다. 이 경우에 fetch 를 수행하기 전에 원격 참조의 "저장 지점" 복사본을 생성할 수 있다. 그런 다음 --force-with-lease 에 이 참조를 기준으로 하도록 지시할 수 있다.

이를 위해 git 의 update-ref 기능을 사용해 리베이스 또는 fetch 작업을 수행하기 전에 원격상태를 저장하기 위한 새로운 참조를 생성한다. 이것은 우리가 강제 푸시를 하기 위한 작업의 시작점을 북마크하는 것과 같다. 여기서 우리는 원격 브랜치 dev 의 현재 참조를 dev-pre-rebase 라는 이름으로 저장하게 된다.

bob$ git update-ref refs/dev-pre-rebase refs/remotes/origin/dev

이제 우리는 리베이스, fetch 등 우리가 원하는 작업을 수행한 뒤에 저장해둔 원격 참조를 활용하여 작업 도중 원격 브랜치에 새로운 변화가 있는지를 파악할 수 있게 된다.

bob$ git rebase master

First, rewinding head to replay your work on top of it...
Applying: Dev commit #1
Applying: Dev commit #2
Applying: Dev commit #3

bob$ git fetch

remote: Counting objects: 3, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /tmp/repo
   2203121..a9a35b3  dev        -> origin/dev

bob$ git push --force-with-lease=dev:refs/dev-pre-rebase

To /tmp/repo
 ! [rejected]        dev -> dev (stale info)
error: failed to push some refs to '/tmp/repo'

이처럼 --force-with-lease 는 가끔씩 강제 푸시를 해야하는 git 사용자한테 매우 유용한 도구이다. 하지만 이는 --force 의 모든 위험상황에 대한 보완책이며, 내부 작동 방식과 주의사항을 이해하지 않은 상태에서 사용해서는 안된다.

하지만 일반적인 사용 상황에서는, 개발자들이 평소처럼 푸시하고 풀링하며 때때로 리베이스를 수행하는 경우에도 --force-with-lease 는 파괴적인 강제 푸시에 대한 필요한 보호 기능을 제공해준다.

단어

caveat: 주의사항, 경고
subtle: 미묘한

profile
Programmer + Poet = Proet

0개의 댓글