Skip to content

Split View: git history — Git이 rebase 옆에 놓은 실험적 history 재작성 명령어

|

git history — Git이 rebase 옆에 놓은 실험적 history 재작성 명령어

들어가며 — rebase 옆에 문이 하나 더 생겼다

세 번째 전 커밋에 빠뜨린 한 줄을 넣고 싶다. 지금 Git으로 이걸 하려면 손이 이렇게 갑니다 — git commit --fixup으로 fixup 커밋을 만들고, git rebase --autosquash로 접고, 그 위에 쌓아둔 브랜치들이 있다면 --update-refs가 제대로 먹었는지 확인한다. 커밋 하나를 둘로 쪼개는 건 더 심합니다. 인터랙티브 rebase를 띄우고, 인스트럭션 시트에서 edit으로 바꾸고, git reset HEAD~로 커밋을 풀고, 두 번 나눠 스테이징해서 두 번 커밋하고, rebase를 끝낸다.

Git 2.54에 이 작업들을 겨냥한 새 명령어가 실험적으로 들어왔습니다. 이름은 git history입니다. 2.55에서 서브커맨드가 하나 더 붙었고요. 이 글은 실제로 뭐가 들어왔는지, 밑바닥이 어떻게 지어졌는지, 그리고 어디서 막히는지를 릴리스 노트·문서·커밋 메시지·소스 같은 1차 출처로 확인해서 정리합니다.

먼저 결론부터 말하면, 이건 "rebase를 대체하는 물건"이 아닙니다. 문서가 스스로 그렇게 말합니다.

무엇이, 언제 들어왔나

타임라인은 이렇습니다. 날짜는 git/git 저장소의 태그 tagger 날짜 기준입니다.

시점내용
2026-01-13Patrick Steinhardt가 builtin: add new "history" command 커밋으로 명령어 도입 (reword 포함)
2026-03-02split 서브커맨드 구현
2026-04-20Git 2.54.0 릴리스 — git historyreword + split로 처음 배포됨
2026-04-27fixup 서브커맨드 구현
2026-06-29Git 2.55.0 릴리스 — fixup 추가

2.54 릴리스 노트의 표현은 딱 한 줄입니다 — "git history" history rewriting (experimental) command has been added. 2.55 노트도 한 줄이고요 — "git history" learned "fixup" command. 릴리스 노트가 이 정도로 건조하다는 게 오히려 현재 상태를 잘 말해 줍니다.

2.55 기준 현재 서브커맨드는 셋입니다.

git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
  • reword — 해당 커밋의 메시지만 고칩니다. 나머지는 그대로.
  • split — 커밋을 인터랙티브하게 둘로 쪼갭니다. hunk를 골라 새 부모 커밋으로 빼냅니다. (이 글에서 hunk는 원어로 씁니다. 뒤에 나오는 훅/hook과 헷갈리지 않게요.)
  • fixup스테이징된 변경을 지정한 커밋에 밀어 넣습니다. 문서 표현으로는 git commit --fixup=<commit> 다음에 git rebase --autosquash <commit>~를 한 것과 성격이 비슷합니다.

git history split HEAD를 실행하면 git add -p와 같은 hunk 선택 프롬프트가 뜨고, 고른 hunk가 새 부모 커밋으로 빠집니다. 문서의 예시에서 bar만 골라 빼내면 원본 커밋은 foo만 남고, 빠져나온 bar 커밋이 그 부모가 됩니다. 전부 고르거나 아무것도 안 고르는 건 한쪽이 빈 커밋이 되므로 거부됩니다.

왜 만들었나 — 커밋 메시지에 적혀 있다

동기를 추측할 필요가 없습니다. 도입 커밋 메시지에 Steinhardt가 직접 적어 뒀습니다. rebase로 흔히 하는 작업들(커밋 순서 뒤집기, 커밋 쪼개기, 커밋 버리기, 여러 커밋 합치기, 팁이 아닌 커밋 수정하기)을 나열한 뒤 이렇게 씁니다.

While these operations are all doable, it often feels needlessly kludgey to do so by doing an interactive rebase, using the editor to say what one wants, and then perform the actions.

그리고 결정적인 문단이 이어집니다.

Rebases also do not update dependent branches. The use of stacked branches has grown quite common with competing version control systems like Jujutsu though, so it clearly is a need that users have.

Git 커밋 메시지가 Jujutsu를 "경쟁 VCS"로 호명하면서, 거기서 흔해진 워크플로가 사용자 수요의 증거라고 말하고 있습니다. 이 정도로 노골적인 인정은 흔치 않습니다.

split 커밋 메시지는 더 실무적입니다. 커밋 쪼개기의 기존 7단계 절차를 쭉 적어 놓고 이렇게 맺습니다.

This is quite complex, and overall I would claim that most people who are not experts in Git would struggle with this flow.

즉 이 명령어의 타깃은 "Git 전문가가 아닌 사람"입니다. 문서도 같은 말을 합니다 — git history는 rebase보다 더 의견이 강하고(opinionated) 더 단순한 방법을 지향하고, 커밋 범위를 다른 베이스에 얹거나 여러 커밋을 한 번에 편집하고 싶으면 rebase를 쓰라고 명시합니다.

rebase와 실제로 뭐가 다른가

문서가 꼽는 차이는 셋인데, 하나씩 뜯어볼 값이 있습니다.

1. 워킹 트리와 인덱스를 안 건드린다. rewordsplit베어 저장소에서도 동작합니다. fixup만 예외인데, 스테이징된 변경을 인덱스에서 읽어야 하기 때문입니다. rebase가 매 커밋마다 워킹 트리를 갈아엎는 것과 대비됩니다.

2. 자손 브랜치를 기본으로 갱신한다. --update-refs=branches가 기본값이고, 원본 커밋의 자손인 로컬 브랜치가 전부 재작성된 커밋을 가리키도록 옮겨집니다. rebase의 --update-refs는 명시적으로 켜야 하는 옵션이지만, 여기선 기본 동작입니다. --update-refs=headHEAD만 옮길 수도 있습니다.

3. 훅을 실행하지 않는다. 문서 원문은 이렇습니다 — git-history(1) does not execute any githooks(5) at the current point in time. This may change in the future. 이건 장점이 아니라 그냥 현재 상태입니다. 뒤에서 다시 다룹니다.

여기에 문서에 없는, 소스를 봐야 보이는 차이가 하나 더 있습니다. ref 갱신이 원자적입니다. builtin/history.c는 갱신 대상 ref를 전부 모아 단일 ref_transaction에 담고 마지막에 한 번 커밋합니다. 스택 브랜치 다섯 개가 걸려 있으면 다섯 개가 다 옮겨지거나 하나도 안 옮겨집니다. 중간에 깨진 상태가 남지 않습니다.

--dry-run도 실용적입니다. ref를 건드리지 않고 git update-ref --stdin이 그대로 먹을 수 있는 형식으로 갱신 계획을 출력합니다. 필요한 객체는 이미 저장소에 써 두기 때문에, 출력된 걸 나중에 적용해도 안전하다고 문서는 설명합니다. 재작성 결과를 미리 보고 싶을 때 쓸 만합니다.

fixup의 세 갈래 병합은 어느 방향인가

fixup이 재미있는 부분입니다. 소스의 주석이 스스로를 설명합니다.

/*
 * Perform the three-way merge to reapply changes in the index onto the
 * target commit. This is using basically the same logic as a
 * cherry-pick, where the base commit is our HEAD, ours is the original
 * tree and theirs is the index tree.
 */
init_basic_merge_options(&merge_opts, repo);
merge_opts.ancestor = "HEAD";
merge_opts.branch1 = argv[0];
merge_opts.branch2 = "staged";
merge_incore_nonrecursive(&merge_opts, head_tree,
                          original_tree, index_tree, &merge_result);

방향을 정리하면 이렇습니다.

base   (머지 베이스) = HEAD의 트리
ours   (side1)      = 대상 커밋의 트리
theirs (side2)      = 인덱스 트리 (= HEAD 트리 + 스테이징된 변경)

왜 이게 맞는지 생각해 보면 깔끔합니다. base가 HEAD이고 theirs가 인덱스이므로, theirs가 base 대비 가진 변경분은 정확히 당신이 스테이징한 diff입니다. 그리고 ours는 대상 커밋의 트리고요. 그래서 이 병합의 결과는 "대상 커밋의 트리 + 스테이징한 diff"가 됩니다. 스테이징한 변경을 과거 커밋 위로 체리픽하는 것과 같습니다. 주석이 "basically the same logic as a cherry-pick"이라고 말하는 이유입니다.

핵심은 merge_incore_nonrecursive라는 이름의 incore입니다. 병합이 전부 메모리에서 일어나고 워킹 트리를 거치지 않습니다. rebase가 커밋마다 파일을 체크아웃했다 되돌리는 것과 근본적으로 다른 지점입니다.

병합이 깨끗하지 않으면 어떻게 될까요. 코드가 아주 짧게 답합니다.

if (!merge_result.clean) {
    ret = error(_("fixup would produce conflicts; aborting"));
    goto out;
}

그냥 중단합니다. 이건 버그가 아니라 설계입니다. 이 얘기는 뒤에서 이어집니다.

--empty 옵션도 이 맥락에서 나옵니다. fixup 결과로 커밋이 비어 버리는 경우가 둘 있는데 — 스테이징한 변경이 대상 커밋의 변경을 정확히 상쇄하거나, 자손 커밋이 방금 조상에 밀어 넣은 것과 같은 변경을 담고 있었거나 — 기본값 drop은 빈 커밋을 없애고, keep은 남기고, abort는 에러로 멈춥니다. 다만 루트 커밋을 드롭하는 건 아직 안 됩니다. 소스의 TODO 주석은 이걸 "기술적 제약일 뿐 존재할 이유가 없는 제한"이라고 적어 놨습니다.

밑바닥 — sequencer가 아니라 git replay 위에 지었다

이 부분이 이 기능에서 가장 조용하지만 중요한 결정입니다.

git history가 자손 커밋들을 다시 얹는 일은 replay_revisions()가 합니다. git replay는 2023년 Elijah Newren이 도입해 2.44.0에 처음 실린 실험적 명령어로, 커밋 범위를 새 베이스에 얹되 "워킹 트리와 인덱스를 건드리지 않고" ref는 "원자적 트랜잭션으로" 갱신하는 물건입니다. git history가 물려받은 성질이 정확히 이것들입니다.

그런데 git replay는 원래 빌트인 명령어였지 라이브러리가 아니었습니다. 그래서 Steinhardt는 git history를 만들기 전에 길을 먼저 닦았습니다. 2026-01-13 같은 날 올라온 패치 시리즈 순서를 보면 의도가 그대로 드러납니다.

6aeda3cf5  builtin/replay: move core logic into "libgit.a"
410e37806  replay: small set of cleanups
542577156  replay: support empty commit ranges
48a72f61f  replay: support updating detached HEAD
475ade1cd  wt-status: provide function to expose status for trees
a675183d4  builtin: add new "history" command
d205234cb  builtin/history: implement "reword" subcommand

먼저 git replay의 핵심 로직을 libgit.a로 빼내고, 빈 커밋 범위와 분리된 HEAD 지원을 replay에 채워 넣은 다음, 그 위에 git history를 얹었습니다. 새 포슬린을 만들려고 기존 플러밍을 라이브러리로 승격시킨 겁니다.

이게 왜 눈에 띄냐면, 2025년 Git 기여자 서밋에서 논의된 방향과 정확히 맞아떨어지기 때문입니다. Taylor Blau가 2025-10-06에 메일링 리스트에 올린 서밋 노트 중 "The future of history rewriting" 세션(진행: Phillip Wood)에는 이런 대목이 있습니다.

Git history vs git replay Replay is plumbing used by server. History is porcelain used by user.

그리고 merge-ort와 replay를 쓴 Elijah Newren의 입장이 이렇게 기록돼 있습니다 — 시퀀서를 넘어서고 싶다, 시퀀서는 워킹 트리를 너무 자주 갱신해서 바람직하지 않다, git historygit replay를 쓰는 쪽으로 갈 수 있다. 당시 노트에는 "일단 시퀀서 underpinning으로 실험 버전을 랜딩해 보는" 안도 적혀 있었고, 결론은 "UI를 먼저 랜딩하고 밑바닥은 나중에 반복하자"였습니다.

실제로 배포된 코드를 확인해 보면 결과가 나옵니다. v2.55.0의 builtin/history.csequencer.h를 include하긴 하지만, 거기서 쓰는 건 커밋 메시지 정리 헬퍼인 cleanup_message() 하나뿐입니다. rebase 시퀀서 머신은 호출하지 않습니다. 재작성은 replay_revisions()가, 병합은 merge_incore_nonrecursive()가 합니다. 즉 서밋에서 논의된 "시퀀서 말고 replay" 방향으로 이미 지어져 나왔습니다.

이건 사용자에게 실질적 차이로 돌아옵니다. 시퀀서 기반이었다면 워킹 트리 체크아웃과 상태 파일(.git/rebase-merge/ 같은)이 따라왔을 테고, 베어 저장소 동작은 불가능했을 겁니다.

정직한 한계 — 이건 아직 실험이다

여기서부터가 실제로 중요한 부분입니다. 문서가 LIMITATIONS 섹션을 따로 두고 있고, 그 내용이 가볍지 않습니다.

머지 커밋이 있으면 못 쓴다. 원문은 This command does not (yet) work with histories that contain merges. 대신 git rebase --rebase-merges를 쓰라고 안내합니다. 소스에는 대상 커밋의 자손 중에 부모가 둘 이상인 커밋이 있는지 미리 리브워크로 확인하고 replaying merge commits is not supported yet!으로 거부하는 코드가 있습니다. 머지 커밋이 섞인 브랜치에서 일한다면 이 명령어는 지금 당신 것이 아닙니다.

충돌이 나면 그냥 중단한다. 이건 미구현이 아니라 명시적 설계 선택입니다. 문서 원문이 이유까지 답니다.

Furthermore, the command does not support operations that can result in merge conflicts. This limitation is by design as history rewrites are not intended to be stateful operations. The limitation can be lifted once (if) Git learns about first-class conflicts.

"history 재작성은 상태를 갖는 작업이 아니어야 한다"는 게 핵심입니다. git rebase가 충돌 나면 rebase --continue를 기다리며 저장소를 어정쩡한 상태로 붙잡아 두는 그 모델을, git history는 아예 거부합니다. 대신 성공 아니면 무변경입니다. 괄호 안의 (if)도 눈여겨볼 만합니다 — 저자 본인이 확정으로 쓰지 않았습니다.

훅이 안 돈다. 정책을 훅으로 강제하는 팀이라면 이건 조용한 함정입니다. git history로 재작성한 커밋은 훅을 우회합니다. 서밋 노트에 이 긴장이 왜 있는지 힌트가 있는데, --update-refs가 추가됐을 때 훅이 깨졌던 전례가 언급되고 "그걸 또 반복하고 싶지 않다"는 얘기가 나옵니다. 지금은 아예 안 돌리는 쪽을 택한 상태이고, 문서는 "미래에 바뀔 수 있다"고 열어 뒀습니다.

루트 커밋 드롭 불가. --empty=drop으로 루트 커밋이 비게 되는 경우는 아직 안 됩니다.

그리고 가장 미묘한 지점 하나. 문서는 EXPERIMENTAL이라고 대문자로 외치는데, 런타임에는 아무 경고도 없습니다. command-list.txt에서 git-historymainporcelain으로 등록돼 있어서 git help의 주요 명령 목록에 그냥 나옵니다. 설정 플래그로 잠겨 있지도 않고, 실행하면 경고 한 줄 없이 그냥 동작합니다. git replay가 SYNOPSIS에 (EXPERIMENTAL!)을 박아 둔 것과 비교하면 표식이 더 약합니다. 서밋 노트의 "실험 버전을 랜딩해서 사람들이 써 보게 하자, 호환성은 약속하지 않는다"는 결론을 생각하면 의도된 상태로 보이지만, 스크립트에 넣기 전에 알고는 있어야 합니다. 문서가 THE BEHAVIOR MAY CHANGE.라고 한 걸 그대로 믿는 게 안전합니다.

막힌 곳의 이름 — first-class conflicts

fixup 커밋 메시지의 마지막 문단이 이 글의 실마리를 다시 묶어 줍니다.

Especially the second item limits the usefulness of this command a bit. But there are plans to introduce first-class conflicts into Git, which will help use cases like this one.

"두 번째 항목"은 충돌 시 중단을 말합니다. 그러니까 이 명령어의 유용성이 제한되는 걸 저자도 알고 있고, 그 해법으로 first-class conflicts를 지목합니다. git history 문서도 같은 걸 지목했고요.

first-class conflicts는 충돌을 "해결할 때까지 붙잡아 두는 임시 상태"가 아니라 커밋할 수 있는 일급 객체로 다루는 모델입니다. jj가 이렇게 동작합니다 — 충돌난 상태를 그대로 커밋하고, 그 위에 계속 작업하고, 나중에 부분적으로 풀 수 있습니다. 그러면 fixup이 충돌을 만나도 중단할 필요가 없어집니다. 충돌을 기록하고 넘어가면 되니까요.

그래서 이게 Git에 들어올 가능성이 있느냐. 2025년 기여자 서밋에 이 주제가 세션으로 올라왔고, 진행자가 Martin von Zweigbergk였습니다. jj를 만든 사람입니다. 노트에 기록된 논의 몇 대목이 분위기를 보여 줍니다.

  • Elijah Newren은 스택 중간을 편집하면서 컨텍스트를 저장해 두거나, 충돌 해결을 동료에게 넘기기 위해 first-class conflicts를 원한다고 말합니다. 큰 충돌을 나눠서 정복할 수 있다는 게 이유입니다.
  • jj가 지금 쓰는 방식도 설명됩니다 — 트리에 특수 헤더를 쓰고 OS가 숨기는 디렉터리에 .left/, .right/ 같은 관례로 충돌을 저장하며, 그 특수 헤더가 있으면 클라이언트가 푸시를 거부합니다. jj가 굳이 이 특수 트리를 쓰는 이유는 충돌 객체가 GC되면 안 되기 때문이고, Git이 스스로 그걸 안 지우는 법을 배우면 jj가 부릴 마법이 줄어든다는 얘기가 나옵니다.
  • 충돌을 트리가 아니라 커밋 헤더에 담자는 안도 논의됩니다. 헤더에 있으면 트리 전체를 걷지 않고도 충돌 여부를 알 수 있다는 장점이 언급되고요.
  • 그리고 "이게 들어오면 Git 명령어 여럿이 낡은 게 되지 않느냐"는 질문에, Elijah는 이미 rebase를 걷어내고 replay로 다시 시작하는 작업을 하고 있다고 답하고, Patrick이 git history도 같은 얘기라고 덧붙입니다.

노트의 마지막 줄은 이렇습니다.

sounds like no broad opposition should we aim for 3.0?

폭넓은 반대는 없었고, Git 3.0을 겨냥해 볼까 하는 질문으로 세션이 끝납니다. 여기서 분명히 해 둘 것 — 이건 합의된 로드맵이 아니라 서밋에서 나온 질문입니다. 일정도, 확정된 설계도 없습니다. 문서가 once (if) Git learns about first-class conflicts라고 쓴 것과 같은 온도로 읽어야 맞습니다.

그래서, 지금 써야 하나

써 볼 만한 경우

  • Git 2.55 이상이고, 선형 history에서 일한다.
  • 스택 브랜치를 쌓아 두고 중간 커밋을 자주 고친다. 자손 브랜치를 원자적으로 같이 옮겨 주는 게 여기서 실제 이득입니다.
  • 메시지 오타 수정(reword)처럼 위험이 거의 없는 작업. 베어 저장소에서도 되는 게 덤입니다.
  • 커밋 쪼개기(split)가 손에 안 익어서 매번 7단계 절차를 검색하고 있다.

아직 아닌 경우

  • history에 머지 커밋이 섞여 있다. 그냥 동작하지 않습니다.
  • 충돌이 예상되는 재작성. 중단될 뿐이니 rebase를 쓰는 게 맞습니다.
  • 훅으로 정책을 강제하는 팀. 훅이 안 돕니다.
  • 스크립트나 CI에 박아 넣는 것. 문서가 동작이 바뀔 수 있다고 명시했습니다.
  • 이미 jj를 쓰고 있다. jj가 하는 일의 부분집합이고, jj는 충돌을 중단 없이 다룹니다.

처음 쓸 때는 --dry-run을 붙여서 어떤 ref가 어디로 움직일지 먼저 보는 걸 권합니다. 어차피 객체는 써 두므로 확인 비용이 싸고, 재작성이 손에 익기 전까지는 이게 가장 확실한 안전벨트입니다. git reflog가 여전히 뒤를 봐 준다는 것도 기억해 두면 좋습니다 — Git이 데이터를 어떻게 붙잡아 두는지는 Git은 당신의 데이터를 어떻게 저장하는가 편에서 다뤘습니다.

마치며

정리하면 이렇습니다. git history는 rebase의 대체재가 아니라, rebase로 하기엔 번거로웠던 몇 가지 흔한 작업을 위한 의견이 강한 좁은 도구입니다. 2.54에 reword/split으로 들어왔고 2.55에 fixup이 붙었습니다. 워킹 트리를 안 건드리고, 자손 브랜치를 원자적으로 옮기고, 밑바닥은 시퀀서가 아니라 git replay + 인코어 merge-ort로 지어졌습니다.

동시에 이건 정직하게 미완성입니다. 머지도 못 다루고, 충돌이 나면 멈추고, 훅도 안 돕니다. 그리고 그 한계들이 어디서 풀릴지도 문서와 커밋 메시지에 이미 적혀 있습니다 — first-class conflicts. 그 논의를 서밋에서 이끈 사람이 jj를 만든 Martin von Zweigbergk였다는 것, 그리고 도입 커밋이 동기로 Jujutsu를 직접 지목했다는 것이, 지금 Git에서 벌어지는 일을 요약합니다. 경쟁 VCS가 먼저 증명한 워크플로가 상류로 역류하는 중입니다.

그래서 지금의 git history는 "완성된 기능"이라기보다 랜딩된 질문에 가깝습니다. UI를 먼저 내놓고 반응을 보자는 서밋의 결론 그대로고요. 써 보고 불편한 곳이 있으면, 그게 아직 메일링 리스트에서 정해지는 중인 답일 수 있습니다.

참고 자료

git history — The Experimental History-Rewriting Command Git Put Next to rebase

Introduction — Another Door Next to rebase

You want to add a line you left out of the commit three back. Here's what your hands have to do to pull that off in Git today — make a fixup commit with git commit --fixup, fold it in with git rebase --autosquash, and if you have branches stacked on top, check that --update-refs actually caught them. Splitting one commit into two is worse. Fire up an interactive rebase, change the instruction to edit, unstage the commit with git reset HEAD~, stage and commit it twice in two passes, then finish the rebase.

Git 2.54 added a new command aimed squarely at these tasks, experimentally. It's called git history. 2.55 tacked on one more subcommand. This post checks release notes, docs, commit messages, and source — primary sources — to lay out what actually shipped, how the foundation is built, and where it hits its limits.

The conclusion up front: this is not a "replacement for rebase." The documentation says so itself.

What Shipped, and When

Here's the timeline. Dates are the tagger dates on tags in the git/git repository.

DateDetail
2026-01-13Patrick Steinhardt introduces the command with the builtin: add new "history" command commit (includes reword)
2026-03-02split subcommand implemented
2026-04-20Git 2.54.0 released — git history first ships with reword + split
2026-04-27fixup subcommand implemented
2026-06-29Git 2.55.0 released — fixup added

The 2.54 release notes say it in exactly one line — "git history" history rewriting (experimental) command has been added. The 2.55 notes are one line too — "git history" learned "fixup" command. That the release notes are this dry actually says something about the current state of things.

As of 2.55, there are three subcommands.

git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
  • reword — fixes only that commit's message. Everything else stays as is.
  • split — interactively splits a commit into two. You pick hunks and pull them out into a new parent commit. (This post keeps "hunk" as-is throughout, so it doesn't get confused with "hook," which comes up later.)
  • fixup — pushes staged changes into the specified commit. In the documentation's own words, it's similar in character to running git commit --fixup=<commit> followed by git rebase --autosquash <commit>~.

Run git history split HEAD and you get a hunk-selection prompt just like git add -p; the hunks you pick get pulled out into a new parent commit. In the documentation's example, picking only bar to pull out leaves the original commit with just foo, and the extracted bar commit becomes its parent. Picking everything, or picking nothing, is rejected because one side would end up an empty commit.

Why It Was Built — It's Written in the Commit Message

No need to guess at the motivation. Steinhardt wrote it out himself in the introducing commit message. After listing the operations people commonly do with rebase — reordering commits, splitting a commit, dropping a commit, squashing several commits together, amending a commit that isn't the tip — he writes:

While these operations are all doable, it often feels needlessly kludgey to do so by doing an interactive rebase, using the editor to say what one wants, and then perform the actions.

Then comes the decisive paragraph.

Rebases also do not update dependent branches. The use of stacked branches has grown quite common with competing version control systems like Jujutsu though, so it clearly is a need that users have.

A Git commit message calling Jujutsu a "competing VCS," and citing how common a workflow has become there as evidence of user demand — that's not something you see admitted this openly very often.

The split commit message is more down-to-earth. It lists out the existing seven-step procedure for splitting a commit, then closes with:

This is quite complex, and overall I would claim that most people who are not experts in Git would struggle with this flow.

In other words, the target audience is "people who are not Git experts." The documentation says the same thing — git history aims to be a more opinionated, simpler way to work than rebase, and it explicitly tells you to reach for rebase instead if you want to move a range of commits onto a different base or edit several commits at once.

What Actually Differs From rebase

The documentation names three differences, and each is worth pulling apart.

1. It doesn't touch the working tree or the index. reword and split work even in a bare repository. fixup is the exception, since it has to read staged changes from the index. This is a contrast to rebase, which churns through the working tree on every commit.

2. It updates descendant branches by default. --update-refs=branches is the default, and every local branch that descends from the original commit gets moved to point at the rewritten commit. rebase's --update-refs is an option you have to turn on explicitly, but here it's the default behavior. You can also move only HEAD with --update-refs=head.

3. It runs no hooks. The documentation's own words: git-history(1) does not execute any githooks(5) at the current point in time. This may change in the future. This isn't a virtue — it's just the current state. More on this later.

There's one more difference the documentation doesn't mention — you only see it in the source. The ref update is atomic. builtin/history.c collects every ref that needs updating into a single ref_transaction and commits it once, at the end. If you have five stacked branches hanging off the commit, either all five move or none of them do. No broken halfway state is left behind.

--dry-run is practical too. It doesn't touch any refs, and prints the update plan in a format git update-ref --stdin can consume directly. The documentation explains that since the necessary objects are already written to the repository, it's safe to apply the printed output later. Useful when you want to preview the outcome of a rewrite.

Which Direction Does fixup's Three-Way Merge Run

This is the interesting part of fixup. The comment in the source explains itself.

/*
 * Perform the three-way merge to reapply changes in the index onto the
 * target commit. This is using basically the same logic as a
 * cherry-pick, where the base commit is our HEAD, ours is the original
 * tree and theirs is the index tree.
 */
init_basic_merge_options(&merge_opts, repo);
merge_opts.ancestor = "HEAD";
merge_opts.branch1 = argv[0];
merge_opts.branch2 = "staged";
merge_incore_nonrecursive(&merge_opts, head_tree,
                          original_tree, index_tree, &merge_result);

Here's the direction, laid out.

base   (merge base) = HEAD's tree
ours   (side1)      = the target commit's tree
theirs (side2)      = the index tree (= HEAD's tree + staged changes)

It's clean once you think through why this is right. Since base is HEAD and theirs is the index, what theirs has changed relative to base is exactly the diff you staged. And ours is the target commit's tree. So the result of this merge becomes "the target commit's tree + the diff you staged." That's the same as cherry-picking your staged changes onto a past commit — which is why the comment calls it "basically the same logic as a cherry-pick."

The key is the incore in the name merge_incore_nonrecursive. The merge happens entirely in memory and never touches the working tree — a fundamentally different point from rebase checking files out and reverting them for every commit.

What happens if the merge isn't clean? The code answers very briefly.

if (!merge_result.clean) {
    ret = error(_("fixup would produce conflicts; aborting"));
    goto out;
}

It just aborts. This isn't a bug — it's the design. More on this later.

The --empty option shows up in this context too. There are two cases where fixup leaves a commit empty — the staged changes exactly cancel out the target commit's changes, or a descendant commit already contained the same change you just pushed into an ancestor — and the default drop removes the empty commit, keep leaves it, and abort stops with an error. Dropping the root commit, though, still doesn't work. The source's TODO comment describes this as "a limitation that's purely technical and has no reason to exist."

The Foundation — Built on git replay, Not the Sequencer

This is the quietest but most important decision behind this feature.

The job of replaying descendant commits in git history is done by replay_revisions(). git replay is an experimental command Elijah Newren introduced in 2023, first shipped in 2.44.0, that puts a range of commits onto a new base "without touching the working tree or the index" and updates refs "in an atomic transaction." These are exactly the properties git history inherits.

But git replay was originally a builtin command, not a library. So Steinhardt paved the road before building git history. The order of the patch series posted on 2026-01-13 shows the intent plainly.

6aeda3cf5  builtin/replay: move core logic into "libgit.a"
410e37806  replay: small set of cleanups
542577156  replay: support empty commit ranges
48a72f61f  replay: support updating detached HEAD
475ade1cd  wt-status: provide function to expose status for trees
a675183d4  builtin: add new "history" command
d205234cb  builtin/history: implement "reword" subcommand

First, pull git replay's core logic out into libgit.a; fill in support for empty commit ranges and detached HEAD in replay; then build git history on top. Existing plumbing got promoted into a library in order to build a new porcelain.

Why this stands out is that it lines up exactly with the direction discussed at the 2025 Git Contributor Summit. In the summit notes Taylor Blau posted to the mailing list on 2025-10-06, the "The future of history rewriting" session (led by Phillip Wood) has this passage:

Git history vs git replay Replay is plumbing used by server. History is porcelain used by user.

And Elijah Newren, who wrote merge-ort and replay, is recorded taking this position — he wants to move past the sequencer, the sequencer updates the working tree too often to be desirable, and git history could go the route of using git replay. The notes from that session also mention an option to "land an experimental version on the sequencer underpinning for now," and the conclusion was "land the UI first, iterate on the foundation later."

Checking the actual shipped code gives you the answer. v2.55.0's builtin/history.c does include sequencer.h, but the only thing it uses from there is cleanup_message(), a commit-message cleanup helper. It never calls into the rebase sequencer machinery. The rewriting is done by replay_revisions(), the merging by merge_incore_nonrecursive(). In other words, it was already built in the "replay, not the sequencer" direction discussed at the summit.

This comes back to users as a real difference. Had it been sequencer-based, working-tree checkouts and state files (like .git/rebase-merge/) would have come along with it, and running on a bare repository would have been impossible.

The Honest Limitations — This Is Still an Experiment

This is where things actually matter. The documentation has a dedicated LIMITATIONS section, and its contents aren't trivial.

It doesn't work if there's a merge commit. The original text: This command does not (yet) work with histories that contain merges. It directs you to use git rebase --rebase-merges instead. The source checks in advance, via a revwalk, whether any descendant of the target commit has more than one parent, and rejects the operation with replaying merge commits is not supported yet!. If you work on a branch with merge commits mixed in, this command isn't for you right now.

If a conflict occurs, it just aborts. This isn't a missing feature — it's an explicit design choice. The documentation itself gives the reason.

Furthermore, the command does not support operations that can result in merge conflicts. This limitation is by design as history rewrites are not intended to be stateful operations. The limitation can be lifted once (if) Git learns about first-class conflicts.

The core of it is that "history rewrites shouldn't be stateful operations." git history rejects outright the model where git rebase, on hitting a conflict, leaves the repository sitting in limbo waiting for rebase --continue. Instead it's success or no change at all. The (if) in parentheses is worth noticing too — the author himself didn't write it as a certainty.

Hooks don't run. For a team that enforces policy through hooks, this is a quiet trap. Commits rewritten with git history bypass hooks entirely. The summit notes give a hint at why this tension exists — they mention a precedent where hooks broke when --update-refs was added, and the note that "we don't want to repeat that." For now, the choice made is to not run them at all, and the documentation leaves the door open, saying "this may change in the future."

Can't drop the root commit. The case where --empty=drop would leave the root commit empty still doesn't work.

And the subtlest point of all. The documentation shouts EXPERIMENTAL in capitals, but there's no warning at runtime at all. In command-list.txt, git-history is registered as mainporcelain, so it just shows up in git help's list of primary commands. It's not gated behind a config flag either — run it and it just works, without a single warning line. Compared to git replay, which stamps (EXPERIMENTAL!) right into its SYNOPSIS, the marking here is weaker. Given the summit notes' conclusion of "land an experimental version so people can try it, with no compatibility promise," this looks intentional — but you should know it before you put it in a script. It's safest to take the documentation's THE BEHAVIOR MAY CHANGE. at face value.

The Name of the Blocker — First-Class Conflicts

The last paragraph of the fixup commit message ties this post's threads back together.

Especially the second item limits the usefulness of this command a bit. But there are plans to introduce first-class conflicts into Git, which will help use cases like this one.

The "second item" refers to aborting on conflict. So the author knows this command's usefulness is limited, and points to first-class conflicts as the fix. The git history documentation points to the same thing.

First-class conflicts is a model that treats a conflict not as "a provisional state you hold onto until it's resolved" but as a first-class object you can commit. That's how jj works — it commits the conflicted state as is, lets you keep working on top of it, and lets you resolve it partially later. Under that model, fixup wouldn't need to abort on hitting a conflict — it could just record the conflict and move on.

So is there any chance this lands in Git? The topic got its own session at the 2025 Contributor Summit, led by Martin von Zweigbergk — the creator of jj. A few passages recorded in the notes give a sense of the mood.

  • Elijah Newren says he wants first-class conflicts so he can preserve context while editing the middle of a stack, or hand off conflict resolution to a colleague. The reason given is being able to divide and conquer a large conflict.
  • The notes also describe how jj currently does it — it writes a special header into the tree and stores the conflict using a .left/, .right/-style convention in an OS-hidden directory, and clients refuse to push when that special header is present. The reason jj bothers with this special tree is that conflict objects must not get garbage-collected, and the notes point out that once Git itself learns not to delete them, jj has less magic left to perform.
  • A proposal to store conflicts in the commit header rather than the tree comes up too, with the advantage noted that you can tell whether a conflict exists without walking the whole tree.
  • And to the question "wouldn't this make a bunch of Git commands obsolete if it lands," Elijah answers that he's already working on tearing out rebase and starting over with replay, and Patrick adds that git history is the same story.

The last line of the notes reads:

sounds like no broad opposition should we aim for 3.0?

No broad opposition, and the session ends on the question of whether to aim for Git 3.0. To be clear here — this is a question raised at the summit, not an agreed-upon roadmap. There's no timeline and no finalized design. It should be read with the same temperature as the documentation's once (if) Git learns about first-class conflicts.

So, Should You Use It Now

Worth trying

  • You're on Git 2.55 or later and work on linear history.
  • You stack branches and frequently fix up commits in the middle. Moving descendant branches atomically along with it is a real win here.
  • Near-zero-risk work like fixing a typo in a message (reword). Working in a bare repository too is a bonus.
  • Splitting a commit (split) never became muscle memory, and you look up the seven-step procedure every time.

Not yet

  • History has merge commits mixed in. It just won't work.
  • Rewrites where a conflict is expected. It'll just abort, so rebase is the right call.
  • A team that enforces policy through hooks. Hooks don't run.
  • Baking it into a script or CI. The documentation explicitly states the behavior may change.
  • You're already using jj. This is a subset of what jj does, and jj handles conflicts without aborting.

The first time you use it, attach --dry-run and see which refs would move where before committing. Objects get written either way, so checking is cheap, and until the rewrite becomes second nature, this is your surest seatbelt. It's also worth remembering that git reflog still has your back — how Git holds onto your data is covered in How Git Actually Stores Your Data.

Closing

To sum up: git history isn't a replacement for rebase — it's a narrow, opinionated tool for a handful of common tasks that were cumbersome to do with rebase. It landed with reword/split in 2.54, and fixup got added in 2.55. It doesn't touch the working tree, it moves descendant branches atomically, and its foundation is built on git replay plus in-core merge-ort rather than the sequencer.

At the same time, it's honestly unfinished. It can't handle merges, it stops on conflict, and hooks don't run. And where those limits will get resolved is already written into the documentation and the commit messages — first-class conflicts. That the person who led that discussion at the summit was Martin von Zweigbergk, the creator of jj, and that the introducing commit named Jujutsu directly as the motivation, sums up what's happening in Git right now. A workflow a competing VCS proved out first is flowing back upstream.

So git history as it stands now is less "a finished feature" and more a landed question. That's exactly the summit's conclusion of shipping the UI first and watching the reaction. If you try it and hit something uncomfortable, that friction might be an answer that's still being worked out on the mailing list.

References