이미 git push로 올려버린 커밋을 없애고 싶을 때가 있습니다.
예: 커밋 메시지를 잘못 썼다, Co-author가 잘못 붙었다, 작업 단위를 나누고 싶다.
위같은 원인들이 유명하죠.
변경된 코드는 그대로 두고, 히스토리에서만 해당 커밋들을 없애는 방법을 정리했습니다.
커밋을 없애거나, 원격 브랜치 내용을 바꾸는 것을 AI에게 해줘~ 하다간 큰일 날 수 있습니다.
그러나 이건 3가지 전제조건이 필요한 매우 조심해야하는 방법입니다.
애시당초에 커밋과 푸쉬는 조심해야하죠.
Git도 결국 “일관된 히스토리(=데이터)” 를 여러 사람이 공유하는 시스템이며
DB에서 롤백 + 강제 반영 같은 고위험 작업에 가깝습니다.
전제 조건 3개가 필요합니다.
- 되돌릴 커밋이 이미 원격에 push된 상태
- 아직 그 브랜치를 pull 받은 사람이 없음 (혼자 쓰는 브랜치일 때만 안전)
- 되돌릴 커밋 개수를 알고 있음 (예: 최근 4개)
3단계긴 한데.. 암기하는 것은 의미가 없음.
단계 명령 하는 일
| 1 | git reset --soft HEAD~N | 최근 N개 커밋만 취소, 변경사항은 스테이징에 유지 |
| 2 | git reset | 스테이징 해제 → 변경사항은 작업 디렉터리만 (스테이징 전 상태) |
| 3 | git push --force-with-lease origin <브랜치명> | 원격 브랜치를 로컬과 맞춰서, push했던 N개 커밋이 원격에서도 사라지게 함 |
N은 되돌릴 커밋 개수, <브랜치명>은 예: feat/iss177.
원격 브랜치와 혹시 로컬 브랜치가 헷갈린다면
쉽게 말하면 로컬 브랜치는 우리의 vscode에 박혀있는 개발용 브랜치이고
원격 브랜치는 깃허브 저장소내 브랜치입니다.
그래서 대부분 네이밍을 아래처럼 합니다.
- 로컬 브랜치: feat/iss177
- 원격 브랜치: origin/feat/iss177
1단계: git reset --soft HEAD~N
git reset이 무엇인가?
일단 reset 명령어는 3가지를 건들 수 있습니다.
- 브랜치 포인터(HEAD)
- 스테이징(Index)
- 작업 디렉터리(working tree)
reset은 옵션에 따라 1~3 중 어디까지 되돌릴지 정합니다.
어려우니까 아래 두 개를 기억하면 쉽습니다.
브랜치 포인터 :git reset --soft HEAD~N
커밋(히스토리) 취소가 핵심
- “최근 N개 커밋이 취소된 것처럼 보임” (브랜치 포인터가 뒤로 감)
- 그 커밋에 들어있던 변경사항은 staged로 그대로 남음
그래서 보통 “커밋을 다시 정리해서 재커밋”하려고 씁니다.
아무것도 없이 : git reset
스테이징 해제가 핵심
그냥 로컬에서 스테이지 해제 많이 하시잖아요 그거입니다.
HEAD는 뭐죠? 머리?
HEAD는 가장 최신 커밋입니다.
- HEAD~N: 현재 HEAD에서 N개 커밋 앞 커밋을 가리킨다.
- reset --soft: HEAD만 그 커밋으로 옮기고, 스테이징·작업 디렉터리는 건드리지 않는다.
결과
- 브랜치가 “N개 커밋 전”을 가리키게 되어, 히스토리상 최근 N개 커밋이 사라진 것처럼 보인다.
→ 중요한 것은 완전히 사라진 것은 아닙니다. reset --soft는 “커밋 객체를 삭제”하는 게 아니라 브랜치 포인터(HEAD)를 과거 커밋으로 옮기는 것입니다.
- 그 N개 커밋에 담긴 모든 변경사항은 Git이 스테이징 영역에 그대로 남겨둔다. (파일 내용도 그대로)
→ - -hard로 하면 변경사항까지 싹 다 날라가겠죠.. 조심해야합니다!
예
# 최근 4개 커밋을 취소
git reset --soft HEAD~4
2단계: git reset (스테이징에서 변경 사항으로 내리기)
의미
- git reset(경로 없이) = git reset HEAD = 스테이징만 HEAD가 가리키는 커밋 기준으로 되돌린다 (기본이 -mixed).
- 1단계에서 HEAD는 이미 N개 전으로 옮겨뒀으므로, “그 커밋 기준”으로 스테이징을 비우는 효과가 난다.
결과
- 스테이징에 있던 변경사항이 작업 디렉터리로만 내려간다.
- 파일 내용은 그대로, “아직 스테이징하지 않은 변경” 상태가 된다.
3단계: git push --force-with-lease origin <브랜치명>
의미
- force push: 원격 브랜치를 로컬 브랜치와 똑같이 덮어쓴다. (히스토리가 바뀐다.)
- -force-with-lease: 원격이 “내가 알고 있던 최신 커밋”과 같을 때만 덮어쓴다. 그 사이에 누가 push했으면 거절해서, 실수로 남의 커밋을 지우는 걸 줄인다.
→ 쉽게 말하면 안전장치인셈 입니다.
git push 진정한 의미론
일반 git push는 보통 앞으로만(fast-forward) 원격 브랜치를 당깁니다.reset으로 로컬이 과거로 가버리면 원격보다 뒤처져서 일반 push가 거절되고, 그걸 원격까지 “뒤로 감으려면” -force-with-lease 같은 강제 업데이트가 필요한 거고요.
또한 워낙 git add . ⇒ commit -m ⇒ push가 일반적이라서 대개 add 단위로 푸쉬가 가능한 줄 아는데, 전혀 그렇지 않습니다. push의 단위는 HEAD가 가리키고 있는 커밋과 원격 브랜치(origin/이후)의 최신 HEAD가 다를때 push가 일어나는 겁니다.
즉, 1 → 2 단계에서 하면서 변경사항을 스테이징된 것이 없는데 어떻게 푸쉬가 되나요? 라는 질문에
답할 수 있을 겁니다. 1 → 2 단계에서 강제로 git reset HEAD~N을 하였기에 로컬 브랜치에서 HEAD가 바뀌었고, 그에 따라 자동적으로 origin과 불일치가 일어났기에 push에 의미가 생긴 겁니다.
결과
- 원격 <브랜치명>이 로컬과 같은 커밋을 가리키게 된다.
- 원격에 있던 “방금 되돌린 N개 커밋”은 그 브랜치 히스토리에서 사라진다.
예
git push --force-with-lease origin feat/iss177
그럼 위의 4개의 커밋은 완전히 사라지는가?
깃허브의 위대함이죠.
아래 중 하나라도 해당되면 그 커밋은 그대로 존재합니다.
- 누군가가 이미 그 브랜치를 clone/pull 해서 로컬에 갖고 있음
- → 그 사람 로컬엔 커밋이 남아있고, 다시 push해서 “부활”도 가능
- PR, 태그(tag), 다른 브랜치가 그 커밋을 참조하고 있음
- → 완전히 안 없어짐
- 내 로컬에서도 보통 reflog에 한동안 남음
- → 실수로 되돌리기도 가능
다시 강조하는 쓰기 전 주의사항
- 이미 그 브랜치를 pull 받은 사람이 있으면 force push 후 그쪽에서 꼬이면 욕 뒤지게먹습니다.
- 안전한 브랜치에서 하셔야합니다.
- main/master 등 공용 브랜치에는 정말 지양하셔야 겠습니다.
