Git history 명령어는 더 많은 관심을 받을 자격이 있다
(lalitm.com)- Git 2.54와 2.55에 걸쳐 도입된 실험적
git history명령은 기존 Git 작업 흐름을 유지하면서 과거 커밋 수정과 파생 브랜치 재구성을 자동화함 fixup은 스테이징한 변경을 과거 커밋에 합친 뒤, 해당 커밋에서 파생된 모든 로컬 브랜치를 새로운 커밋 해시에 맞춰 자동 리베이스함reword는 과거 커밋 메시지를 바꾸고,split은 커밋의 변경 내역을 헝크 단위로 나눠 두 커밋으로 분리한 뒤 후속 커밋과 브랜치를 다시 구성함- 세 하위 명령은 충돌 가능성이 있는 작업을 거부하는 원자적 처리로 작업 트리가 중간에 깨지는 상황을 막지만, 병합 커밋이 있는 이력에서는 동작하지 않음
jj의 1급 충돌 처리, 작업 로그 기반 실행 취소, 작업 복사본의 커밋 모델링까지 제공하지는 않지만, 별도 설치 없이 Git 코어 배포판에서 여러jj식 이력 편집 기능을 사용할 수 있음
기존 Git 이력 편집이 불편한 이유
- 여러 변경을 병렬로 다루면 브랜치와 커밋을 계속 오가야 하며,
rebase -i도중 작업 트리가 불완전한 상태에 빠질 수 있음 - Git 대안인
jj는 이런 문제의 해결책으로 자주 거론되지만, 1년 반 동안 3개월마다 며칠씩 시도해도 결국 기존 Git 작업 흐름으로 돌아오게 됨 git history는 Git 코어 배포판에 포함된 실험적 명령이어서 별도 도구를 설치하지 않고 시험할 수 있음- 하위 명령은
fixup,reword,split세 가지로 구성됨
fixup: 과거 커밋과 파생 브랜치 동시 수정
git history fixup은 과거 커밋의 잘못된 부분을 수정하고, 그 커밋에서 파생된 브랜치를 자동으로 리베이스함- 일반적인 수정 절차처럼
git add로 변경을 스테이징한 뒤git history fixup <commit>을 실행하면 스테이징된 변경이 대상 커밋에 합쳐짐 git commit --fixup과 autosquash 리베이스를 합친 것과 비슷하지만, 대상 커밋을 포함하는 다른 브랜치까지 갱신한다는 차이가 있음git rebase --update-refs가 현재 리베이스 범위 안의 참조만 이동하는 데 비해,git history는 대상 커밋에서 파생된 모든 로컬 브랜치를 찾아 다시 작성함- 필요하면 갱신 범위를 현재 브랜치로만 제한할 수 있음
- 병합 커밋이 있는 이력에서는 동작하지 않아 일부 Git 사용 방식에는 적용하기 어려움
B에 수정 사항을 합치면 새로운 해시를 가진B*가 생성되고, 그 위의C와D도 각각C*,D*로 다시 만들어짐feat-1과feat-2의 브랜치 끝도 새 커밋을 따라 이동함
원자적 처리와 충돌 제한
- 세 하위 명령은 모두 원자적으로 처리돼 작업 트리를 반쯤 깨진 상태로 남기지 않음
- 충돌을 일으킬 수 있는 작업 자체를 거부하는 방식으로 원자성을 보장함
- 이는 충돌을 1급 객체로 처리해 리베이스 도중 충돌 상태를 유지하고 나중에 해결할 수 있는
jj보다 기능적으로 제한적임 git history제한 사항은 이력 재작성이 상태 보존 작업으로 바뀌지 않도록 의도적으로 적용된 것임- Git이 향후 1급 충돌을 지원하면 이 제한을 해제할 수 있음
reword: 작업 트리 없이 커밋 메시지 변경
git history reword는 과거 커밋 메시지를 바꾸고, 그 위의 커밋과 브랜치를 자동으로 다시 구성함git history reword <commit>을 실행하면 편집기에 기존 메시지가 열리며, 수정 후 저장하면 후속 커밋 스택이 새 해시에 맞춰 재구축됨- 반복 개발 중 설계가 바뀌었을 때 이전 커밋 메시지를 현재 구조에 맞게 고치는 데 유용함
- 트리 내용 대신 커밋 메시지만 바꾼다는 점을 제외하면
fixup과 같은 방식으로 동작함 reword는 인덱스나 작업 트리를 건드리지 않고 커밋 그래프만 변경함- 체크아웃하지 않은 브랜치의 커밋도 현재 작업을 방해하지 않고 수정할 수 있음
B의 메시지를 바꾸면 해시가 달라지므로 후속C는C*로 다시 만들어지고feat-1도 새 커밋을 따라감
split: 하나의 커밋을 둘로 분리
git history split은 한 커밋의 변경을 대화식으로 선택해 두 커밋으로 나눔git history split <commit>을 실행하면 대상 커밋의 변경 내역을 헝크 단위로 선택하는 프롬프트가 열림- 선택해 유지한 헝크는 첫 번째 커밋을 구성함
- 나머지 헝크는 두 번째 커밋에 들어감
git add -p와 비슷하지만git rebase를 조합하는 복잡한 절차가 필요 없음- 서로 관련 없는 두 변경이 들어 있던
B를 분할하면B1과B2가 생성되고, 후속C는 두 커밋 위에C*로 재구축됨 - 세 명령 중 사용 범위가 가장 특화돼 있지만, 커밋을 실제로 분리해야 할 때 유용함
jj와의 기능 차이
git history는jj가 제공하는 전체 작업 모델을 대체하지 않음jj는 작업 로그와 간단한 실행 취소를 제공함- 작업 복사본을 커밋으로 모델링함
- 리베이스 과정에서 충돌 상태를 유지할 수 있음
- 반면
git history는 기존 Git 안에서 과거 커밋 수정, 메시지 변경, 커밋 분할과 파생 브랜치 갱신을 지원해jj의 일부 장점을 현재 작업 흐름에 도입할 수 있음 - 향후 제한을 완화할 가능성이 열려 있어 이후 Git 릴리스에서 추가로 개선될 여지가 있음
댓글과 토론
Hacker News 의견들
-
Git이 불편했지만 무료로 공개된 Pro Git 책의 첫 세 장을 읽고 나아졌음. Git 내부 동작에 관한 훌륭한 정신 모형을 제공하며, 좋든 나쁘든 Git의 사용자 인터페이스는 내부 구조를 직접 반영하므로 이를 이해하자 모든 것이 맞아떨어졌다
- 사용자 인터페이스가 내부 구조를 직접 반영한다는 데 동의하지 않음. 가령
git checkout은 브랜치를 전환할 때는 아직 Git에 들어가지 않은 변경을 잃지 않지만, 파일의 다른 버전을 복원할 때는 변경을 되돌리도록 설계됐는데 이것이 어떻게 내부 구조를 직접 반영하는가?
이와 별개로, 내부에서 원하는 작업과 명령을 알아도 정확한 명령줄 구문을 찾기 어렵게 만드는 CLI의 비일관성도 Git을 사용하기 어렵게 함 - Git은 알려진 것만큼 혼란스럽지 않으며, 몇 시간만 들여 이해하면 됨. 소프트웨어 엔지니어의 필수 도구인데도 이를 배우지 않는 것은 안타까우며, 전문가라면
git commit -am “Changes”만으로는 부족하다 - 나도 Git 명령이 혼란스러웠지만, Linus가 Git의 내부 자료 구조를 설명하는 YouTube 영상을 보고 아주 단순하다는 걸 알게 됐음. 도구의 기능이 자료 구조를 어떻게 조작하는지 쉽게 대응시킬 수 있었고, 오히려 Git이 제공하려는 상위 수준 추상화가 상황을 더 혼란스럽게 만든다
- 그렇지 않음. Git 명령의 절반은 저수준이고 나머지 절반은 고수준이므로, 내부 구조와 1:1 대응하지 않는다
- Mercurial의 사용자 경험이 단연 최고였고, 따로 문서를 읽지 않아도 배울 수 있었음. Git을 쓸 때도 Mercurial처럼 사용하다가 문제가 생기면 로컬 저장소를 지우는 방식으로 경력 내내 버텨왔다
- 사용자 인터페이스가 내부 구조를 직접 반영한다는 데 동의하지 않음. 가령
-
git rebase --abort가 있으며, 리베이스 전에 태그를 만들었다가git reset --hard $set_tag로 돌아갈 수도 있음. 이전 상태가 사라지는 게 아니므로 대화형 리베이스를 무서워할 이유가 없다- 바로 그 용도로
temp나before-rebase라는 브랜치를 많이 만들어 둠. 태그처럼 쓰지만 브랜치는 옮기기가 더 간편하므로 큰rebase -i전에 보통git branch before-rebase/some-feature를 실행한다
실제로git reset before-rebase가 필요했던 적은 거의 없지만,git log -p before-rebase를 리베이스 후 브랜치와 비교해 충돌 해결 과정의 의도치 않은 변경을 확인하는 경우는 많았다 - 별도의 안전 태그를 만들 필요조차 없으며, reflog가 자동으로 그 역할을 해준다
git reflog를 사용하는가?jj undo보다 더 간단할 수는 없다- 맞음.
git rebase를 입력한 뒤 절반 정도는 바로--abort를 입력한다
- 바로 그 용도로
-
스스로 코더나 프로그래머라고 생각하지 않지만, Git을 배우는 것은 내 사고방식에 정리의 초능력을 준 것 같았음. 웹사이트, 디자인 프로젝트, 전자공학, 작곡, 개인 지식 저장소, 원격 관리 스크립트, 설정 관리, 코드 조각 등 수많은 용도로 활용하며, 완벽하진 않아도 함께 일하는 모두에게 배우라고 권한다
- 나도 정리 능력이 크게 향상됐지만, 이는 Git 자체보다 버전 관리 시스템 전반의 효과에 가까움. 버전 관리 시스템은 만든 것을 정리하고 보존하며 언제든 원하는 만큼 복사본을 꺼낼 수 있어, 걱정 없이 일하게 해준다
David Allen의 『Getting Things Done』에는 생각을 모으고 정리할 신뢰할 수 있는 시스템이 필요하다는 내용이 있음. 나는 작업·프로젝트·목록에는 OmniFocus를 쓰지만, 컴퓨터에서 만든 소프트웨어와 일부 데이터를 정리하고 보존하는 일은 Git을 신뢰한다 - 작곡에는 Git을 어떻게 사용하는가?
- 나도 정리 능력이 크게 향상됐지만, 이는 Git 자체보다 버전 관리 시스템 전반의 효과에 가까움. 버전 관리 시스템은 만든 것을 정리하고 보존하며 언제든 원하는 만큼 복사본을 꺼낼 수 있어, 걱정 없이 일하게 해준다
-
이 명령들을 이론적으로 매우 좋아하고
jj의 동등한 명령도 늘 사용하지만,git history가 수정한 커밋에 서명하는 방법을 찾지 못했음. 매뉴얼과 Git 소스 코드를 살펴봐도 기존 GPG 서명 커밋을git history reword HEAD~로 수정하면 서명이 모두 사라진다
커밋 서명을 유지하고 싶어서 결국 전통적인git rebase -i로 돌아갔다- 이는 큰 누락처럼 보이며 표준 커밋 처리 과정도 우회하는 듯함. 매뉴얼에서도
git history를 실험적 기능이라고 하니 버그로 신고할 만하다
- 이는 큰 누락처럼 보이며 표준 커밋 처리 과정도 우회하는 듯함. 매뉴얼에서도
-
새 명령 없이도 10년 넘게 로컬 Git 커밋 기록을 큰 어려움 없이 대폭 수정해 왔음. Git과 리베이스가 어렵게 느껴지는 큰 이유는 정확한 명령줄 옵션과 동작 방식을 직접 파악해야 하는 사용자 인터페이스 문제라고 확신한다
Windows에서는 TortoiseGit의 리베이스 대화상자가 강력하면서도 사용하기 쉬웠음. 커밋 로그에서 기준 커밋을 우클릭해"Rebase onto this"를 선택하면 되고, https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm...처럼 커밋의 순서 변경·분할·결합·편집이 명확하며 중간에 취소하면 원래 상태로 돌아간다
다만 셸 확장 방식이라 클릭이 너무 많아 탭이 있는 독립 앱이면 더 좋겠지만, 이는 TortoiseSVN을 계승한 설계 원칙으로 보임. Git Extensions도 잠시 써봤으나 자주 쓰던 기능을 찾지 못했거나 이미 TortoiseGit에 익숙해져 정착하지 못했다. 로그 화면은 https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht...에서 볼 수 있다- Windows에서 Linux로 옮긴 뒤 아쉬운 것 중 하나는 모든 프로그램이 파일·폴더의 우클릭 메뉴, TortoiseGit의 상태 아이콘 같은 오버레이,
My Computer같은 특수 객체를 추가할 수 있었던 통합 셸임. XP에서 정점을 찍었고 Vista부터 Microsoft가 확장하기 어려운 방향으로 바꾼 것으로 이해한다
다만 VC++6의 어떤 메뉴가 일반 목록 대신 폴더처럼 생긴 셸 객체를 만들어 파일 열기 대화상자에서 표시했던 이유는 아직도 이해되지 않음. 프로젝트에 구성 요소를 추가하는 마법사였는데, 대화상자가 열린 동안 그 가짜 폴더를My Computer에서 찾곤 했다 - 기존 작업 흐름이 조금 더 번거로웠고 새 명령이 더 편리한 것은 맞음.
fixup은 이미git commit --fixup이나 그 이전의git rebase -i로 가능했고,reword도git rebase -i로 할 수 있었다
각 커밋에 수행할 작업을 정하려고 파일을 편집하는 과정이 불편하다는 피드백이 충분히 쌓여, 기존 기능을 감싼 상위 수준 명령이 추가된 듯하다 - Git 사용자 인터페이스 중에서는 Magit에 근접한 도구를 본 적이 없음. 깊이 있는 기능과 단순화된 상태 유지형 인터페이스를 함께 제공한다
명령줄 도구는 다음 명령을 실행하면 이전 출력에 접근하기 어려워진다.git log에서 본 커밋을 체리픽하려 해도 해시가 터미널에 남아 있지 않으면 기억만으로 입력할 수 없지만, 상태 유지형 인터페이스는 여러 상호작용을 병렬로 유지하고 이전 정보를 보존해 준다
Git은 현재 브랜치, 직전 브랜치, 수정된 파일, 푸시하지 않은 커밋 등 기억해야 할 상태 정보가 매우 많음. 셸 플러그인의 브랜치 표시와 자동 완성, 여러 탭·창 분할, 탭 간 복사, 자동 완성 검색 같은 주변 도구를 익히지 못하면 Git 사용이 매우 불쾌해질 수 있다
- Windows에서 Linux로 옮긴 뒤 아쉬운 것 중 하나는 모든 프로그램이 파일·폴더의 우클릭 메뉴, TortoiseGit의 상태 아이콘 같은 오버레이,
-
jj의 실질적인 약점은 Git 연동뿐이라고 봄. 파일 집합, 리비전 집합, 템플릿을 위한 도메인 특화 언어를 정형화해 설정과 CLI 전반에서 일관되게 쓰며, 내부 구조를 직접 노출하지 않고 강력한 개념을 인터페이스로 제공한다
하지만 너무 유연해 Git 상호운용성이 관례와 규율에 크게 의존하고, 거의 모든jj상태를 Git으로 표현할 수 있어 원격 저장소를 망치기 쉬움. 이를 복구하려면 Git을 깊이 이해해야 한다
새 단독 개발 프로젝트라면jj를 시도할 만함. 유리한 조건에서 보면 Subversion과 비교한 초기 Git 같은 느낌이며, 먼저jj자체의 방식에 익숙해지면 이후 험난한 Git 연동에도 조금 더 잘 대비할 수 있다. 관련 맥락은 https://news.ycombinator.com/item?id=48903054에 있음- 원칙적으로
jj에는 Git이 아닌 백엔드도 있지만, 현재 운영 환경에 적합하다고 명시된 것은 Git 백엔드뿐임. Git 연동 없는jj는 아직 현실보다 개념에 가깝다
jj와 그 버전 관리 정신 모형을 좋아해 계속 쓰겠지만, 가끔 작업을 끝내려고 Git으로 내려가야 하므로 새로운 형상 관리 시스템보다는 Git 위의 편리한 래퍼처럼 느껴짐. Git이jj작업 흐름을 지원하는 명령을 추가하면jj를 따로 설치할 이유를 찾기 어려울 것이다
- 원칙적으로
-
git history fixup이 모든 후손 브랜치를 자동 리베이스하는 방식은 내 작업 흐름과 맞지 않음. 기능 브랜치의 각 버전을myfeature.1,myfeature.2처럼 보존해 새 버전에서 동작이 달라졌을 때 예전 버전을 찾는데,git reflog로는 이를 대체할 수 없다
예를 들어 문제가myfeature.58과myfeature.59사이에서 생겼고 기능마다 커밋이 15개라면git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59로 기반 커밋이 달라도 두 버전의 차이를 커밋별로 볼 수 있음. 따라서 특정 커밋을 포함한 모든 브랜치의 자동 리베이스를 원하지 않으며, 오래된 버전에 태그를 쓰는 방식도 잘 맞지 않았다 -
최신 Git은 자주 쓰는
git rebase --interactive작업 세 가지를 마찰이 적은 별도 명령으로 구현했으며, 충돌이 없을 때만 동작하는 듯하다git history가 정말 유용한지는 의문임. 나는 대화형 리베이스로 커밋을 시각적으로 옮기는 편을 선호하며, 현재 로컬 브랜치에서 사라진 고아 커밋 해시를 실수로git history대상으로 지정하고 싶지 않다
-
Git 기록을 완벽하게 다듬는 데 왜 그렇게 공을 들이는지 이해하지 못하겠음. 어차피 개별 커밋을 다시 읽지 않으니 병합 전에 모두 스쿼시하면 된다
- 강하게 반대함. PR을 검토할 때 최소한 커밋 기록을 읽어 변경의 흐름을 파악하며,
"oops"와"fix"로 가득하면 즉시 반려한다
커밋은 작업의 이야기를 전달하므로, 검토자가 의도를 이해하려고 더 고생하지 않도록 읽기 좋은 커밋 기록을 제공해야 한다 - 나는 개별 커밋을 자주 읽음. 세심하게 관리된 저장소에서는 버그나 회귀를 추적하고 작성자의 변경 의도를 이해하는 데 매우 유용하다
- 코드가 왜 이렇게 됐는지 궁금할 때 해당 코드를 도입한 커밋의 맥락을 보면 큰 도움이 됨. 서로 다른 변경을 한꺼번에 담은 거대한 커밋이라면 그 가치가 훨씬 떨어진다
- 큰 팀에서 일하지 않는다는 사실이 드러나는 생각임. 휴가에서 돌아오면 가장 먼저 Git 기록을 읽어 그동안 무엇이 바뀌었는지 확인하고, CI가 깨졌을 때도 유용해서 사실상 매일 어떤 형태로든 기록을 살펴본다
코드베이스와 기능이 아주 작다면 모두 스쿼시해도 되겠지만, 나는 실제 변경 흐름을 확인하고 커밋을 선택적으로 체리픽하거나 되돌릴 수 있는 방식을 선호한다 - 논리적 변경 하나당 커밋 하나로 잘 정리된 기록은 회귀를 이분 탐색할 때 대단히 유용하다
- 강하게 반대함. PR을 검토할 때 최소한 커밋 기록을 읽어 변경의 흐름을 파악하며,
-
현재
D를 체크아웃한 상태에서B를B'로 바꿀 때,git rebase --update-refs는 리베이스 범위 밖의E를 그대로 두고B' → C' → D'만 만들며 기존B → E를 유지하는 것으로 이해했음
반면git history fixup은E까지E'로 갱신하는가? 그렇다면 수십 년간 손에 밴git rebase로도 모든 후손 브랜치 갱신을 수행할 방법이 있는지 궁금하다- 커밋 해시는 부모 커밋의 해시도 반영하므로
E를 그대로 둘 수 없음.--update-refs를 직접 써보지는 않았지만 문서대로라면E'까지 생기는 세 번째 그래프가 결과일 것이므로, 원하는 동작은 이미git rebase에서 가능하다 - https://blog.hot-coffee.dev/en/blog/git_update_refs/를 참고하면 된다
- 커밋 해시는 부모 커밋의 해시도 반영하므로