Android/학습

[Git] Git Merge 종류 (with. force push로 commit이 사라졌을 경우)

한때미 2025. 8. 9. 12:24

작업을 하거나 협업을 할 때 브랜치를 만들고 해당 브랜치를 Merge 한 경험은 다들 있을 것이다.

회사에서 Feather 브랜치를 만들고 delvop 같은 브랜치에 merge 하여 자신이 한 작업물을 올리는 등의 각자 회사에서 따르고 있는 git flow가 있을 것이다. 이 부분에서 각자 팀에서 암묵적으로나 명시적으로 merge 규칙 등이 있을 텐데,

한동안 생각 없이 규칙대로만 작업하다 보니 해당 개념을 좀 잡아보려, 이 Merge에 대해서 좀 더 알아보려고 한다.

 


우선 대표적인 3가지 Merge 방법에 대해 알아보고 좀 더 특이 케이스에 대해 보도록 하자.

  • Merge (3-way-merge)
  • Sqush and Merge
  • Rebase and Merge

 

우선 위 3가지 Merge 방법은 GitHub GUI에서 지원해 주는 Merge 방법이다.

 

GitHub Pull Request의 Merge 방법

 


Merge (3-way-merge)

가장 기본적은 Merge 방법이다. 따로 설정하지 않은 경우 대부분 해당 방법으로 Merge가 될 것이다.

  • 현재 브랜치의 commit [HEAD]
  • 병합할 브랜치의 commit [MERGE_HEAD]
  • 공통 조상이 되는 commit [merge base]

이렇게 3가지의 commit을 비교하여 충돌 여부, 병합 방식을 판단하기 때문에 3-way-merge라고 불린다.

 

GitHub의 Create a merge commit 방식도 해당 방식을 따르고 새로운 merge commit을 생성하고 병합할 브랜치의 commit  히스토리를 그대로 보존하는 특징이 있기 때문에 모든 변경사항을 상세히 파악할 수 있다.

 

 

왼쪽 상태의 두 브랜치에서 create a merge commit 진행할 경우 main 브랜치(오른쪽) 상태

 

Sqush and Merge

해당 머지 방식은 병합할 브랜치의 모든 변경 사항을 하나의 commit으로 합쳐서 merge 된다.

앞서 말한 Merge 방식과 전혀 다르게 병합할 브랜치의 작업분을 Merge 했다는 점에 초점을 두는 방식으로,

병합 브랜치의 각 시점의 변경사항 정보가 사리 진다.

 

 

왼족 상태의 두 브랜치에서 squash and merge를 진행할 경우 main 브랜치(오른쪽) 상태

 

Rebase and Merge

해당 방법은 현재 브랜치에 병합할 브랜치의 변경사항의 commit을 추가하여 재배열하는 방식을 말한다.

 

왼쪽 상태의 두 브랜치에서 rebase and merge를 진행할 경우 main 브랜치(오른쪽) 상태

 


프로젝트에서 해당 Merge들의 실제 동작 결과를 확인해 보자.

 

main 브랜치, main 1,2 commit이 작업되어 있다.

 

우선 여러 브랜치를 Merge해 올 main 브랜치의 작업 상태이다.

그래고 아래는 해당 main 브랜치에 합병해 올 각 브랜치 변경상태이다.

 

왼쪽부터 순서대로 merge_commit_test(이하 merge), squash_merge_test(이하 squash), rebase_merge_test(이하 rebase) 브랜치이다.

 

각 브랜치들은 전부 동일하게 main의 fix: build commit 시점(marge test commit 전)에서 분기하여 각자의 test commit 변경사항을 가지고 있다.

 

전체적인 브랜치 관계

 

rebase and merge 방식으로 rebase 브랜치를 merge 하면 다음과 같다.

rebase and merge  방식으로 merge, 왼쪽 merge 후 main, 오른쪽 main과 rebase 브랜치 비교

 

위에서 설명했다시피, rebase에 있던 commit들이 main의 변경사항에 포함된 것을 확인할 수 있다.

rebase를 통해 main 브랜치가 rebase(rebase_merge_test) 브랜치의 변경사항 commit들을 자신의 변경사항인 것처럼 포함시킨 것을 확인할 수 있다.

 

 

다음으로, squash and merge 방식으로 squash 브랜치를 merge 하면 다음과 같다.

squash and merge 방식으로 merge, 왼쪽 merge 후 main, 오른쪽 main과 squash 브랜치 비교

 

rebase와 다르게 merge 후 main에 하나의 commit(squash merge test #3)이 생긴 것을 확인할 수 있는데,

squash 브랜치에 있는 변경사항(2개의 commit)이 하나의 commit(squash merge test #3)으로 합쳐져 병합된 것을 확인할 수 있다.

 

2개의 commit

  • merge test: squash_merge-test 2 commit
  • merge test: squash_merge-test 1 commit

 

참고로 GitHub에서 squash and merge를 진행할 때, 병합 commit을 naming 할 수 있다.

 

GitHub의 squash and merge 진행

 

다음으로는 merge 브랜치를 main에 가장 기본적인 Merge를 진행하면 다음과 같다.

 

Create a merge commit 후 main 브랜치

 

기존 Merge 방식들과 다르게 Merge 브랜치의 변경사항 commit 히스토리가 실제 main 브랜치에 남아 있는 것을 확인할 수 있다.

 

이렇게 GitHub에서 제공해 주는 3가지 Merge 방식을 실제로 테스트해 봤는데, 사실 또 다른 Merge 방식이 존재한다.

Fast-Forward Merge이라고, 이 Merge 방식은 main 브랜치에서 적은 변경사항만이 존재할 경우 쉽게 commit tree를 관리할 수 있게 해 주는데, 이 방식은 GitHub GUI에서는 지원해주지 않는다.

 


Fast-Forward Merge

현재 브랜치에 변경사항이 없고 병합할 브랜치에만 변경사항이 존재할 경우 해당 변경사항을 그대로 가져오는 방법이다.

서로 다른 브랜치를 병합하는 것이 아닌 현재 브랜치의 HEAD를 이동시키는 방법이다.

 

 

 

실제 프로젝트에서 확인해 보면 다음과 같다.

main 브랜치의 최신 상태의 시점에서 commit 2개(3, 4 commit)를 추가한 브랜치가 있다.

최신 main 브랜치에서 브랜치를 따와 새로운 변경사항을 추가한 브랜치

 

main 브랜치에서 해당 브랜치를 Fast-Forward Merge 하면 다음과 같다.

 

Fast-Forward Merge 후 main 브랜치, comit 3, 4가 이어 붙어진 것을 확인 가능

 

빠르게 병합한 브랜치의 변경사항이 main에 머지된 것을 확인할 수 있다.

 

언뜻 보면 rebase and merge와 비슷해 보이나, fast-forward merge는 rebase and merge와 다르게 새로운 commit을 생성하여 재배치하는 것이 아닌 병합하는 해당 브랜치의 commit을 그대로 가져오는 방식이기 때문에 기존 commit ID가 동일하다는 차이점이 존재한다.

 

 

 


추가적으로

 

여러 사람과 협업할 때 공통적으로 많은 사람들이 병합을 진행하는 브랜치가 있을 것이다. (develop, release 브랜치 등등)

그 작업 중 Rebase and Merge를 사용을 자제하라는 권고를 받은 경우가 있을 수도 있다. (우선 우린 그랬다)

왜 그럴까, 해당 부분에서 어떤 위험이 발생할 수 있는지 한번 만들어보도록 하자.

 

 

우선 상상해 보도록 하자.

나는 main에서 브랜치를 따, 작업을 해서 청록색 변경사항을 main 브랜치에 머지할 것이다.

 

내가 작업한 브랜치는 해당 변경사항을 가지고 있다

  • rebase_merge_test 4 commit
  • rebase_merge_test 3 commit

 

main 브랜치의 최신 commit은 다음과 같다

  • merge_commit_test 4 commit

오른쪽 보라색의 main 브랜치와 내가 작업한 rebase 브랜치, 왼쪽 rebase한 rebase 브랜치

 

나는 해당 브랜치 작업을 rebase and merge 하기 위해 main을 rebase 하였다.

이제 main에서 해당 브랜치를 merge 할 거다.

 

아뿔싸 그런데!

 

사실 main 브랜치최신 commit은 merge_commit_test 4 commit이 아니라 추가로 commit 된 main 3 commit 이였던 것이다.

내 로컬에서 해당 최신 값의 반영이 늦어 이 부분을 알지 못하고 나는 옛 버전의 main 브랜치로 rebase를 진행한 것이다.

 

내가 반영하지 못한 추가 commit 존재
원래 최신 main 브랜치 상태

내가 아무것도 모른 상태에서 main의 변경사항 하나(main 3 commit)를 지워버린 것이다.

보통 해당 부분에서 로컬 브랜치와 원격 브랜치의 commit tree가 맞지 않게 때문에 해당 내용의 팝업으로 일반적인 push가 불가능하다.

왼쪽은 내가 merge해버린 main 브랜치, 오른쪽은 push 오류 팝업

 

하지만 보통 rebase를 진행한 경우 commit들이 재정렬되기 때문에 기존 배열과 달라진 commit tree를 push 하기 위해 force push를 진행하는 경우가 많다.

 

그렇다면 이런 대참사가 일어나는 것이다.

깔끔하게 사라진 main 3 comit

 

당황하지 말라

본인이 참사를 일으킨 당사자라면, 아직 진정하고 되돌릴 방법이 있다.

우선, 해당 참사를 일으킨 당사자로서 물의를 일으킨 부분에 대해(나 자신과 모두에게) 석고대죄하며 울면서 참회하길 바란다.

 

로컬에는 해당 작업을 진행한 명령어 기록이 존재한다.

해당 git 명령어는 reflog로, 이때까지 본인이 돌아가고자 하는 hash 값을 찾아 작업을 되돌리도록 하자.

reflog나 checkout, reset 명령어 등으로 해당 소동은 침착하게 복구될 수 있다.

 

기존 브랜치 상태를 복원하든, 작업 로컬 브랜치를 복원하여 되돌리든 어떻게든 방법이 있다.

 

해당 방법으로 해결이 불가능하다면?

해당 작업의 로컬 브랜치가 존재하지 않고 복구도 되지 않는다면?

사실 focus push를 실행 시점 명령어를 reflog로 찾을 수 없다면? (사실 범인은 내가 아니었다?)

 

일단 슬퍼하자. 너무 슬픈 일이다. 처참한 재앙의 현장이 맞다.

 

진짜 내가 focus push 정도야 흥 하는 생각이 얼마나 오만하고 미친 생각이었는지 다시 한번 배울 수 있었다는 점에서 성장 포인트를 얻을 수 있는 경험이다.

만약 내가 사고를 일으킨 당사자 아니라면? 그래서 나는 되도록 로컬 브랜치를 삭제하지 않고 운영배포 전까지 보관한다.

운전도 방어 운전이 중요하듯이 개발도 방어 개발(?)이 중요하다는 점을 또 한번 배울 수 있었다. (알고 싶지 않았다)

 

 

여기서 사라진 commit을 뒤지는 방법이 있다.

git fsck --lost-found

gitHub는 기본 90일 정도(설정값)의dangling commit을 GC 하지 않는다.

git fsck --lost-found를 실행할 경우 dangling commit 내역을 확인할 수 있다.

 

git fsck --lost-found 명령어를 실행하여 dangling commit 확인

 

여기서 git show 명령어를 통해 하나하나 내가 잃어버린 commit인지 확인하여 원하는 commit hash를 찾으면 된다.

제발 존재하길 빌어보자

git show 예시

만약에 원하는 commit을 찾은 경우 해당 commit을 복원하여 브랜치를 생성하는 방법은 다음과 같다.

git checkout -b [브랜치 명] [commit hash]

commit 복원 명령어

 

잃어버린 소중한 commit

 

 

 

이렇게라도 복원할 수 있다면 무척 다행이다.

 


추가적으로 하면 좋은 일

  • 각 Merge 설명 보충

참고 자료

더보기