Skip to content

필사 모드: git history — Git が rebase の隣に置いた実験的な history 書き換えコマンド

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

はじめに — rebase の隣にもう一つ扉ができた

3つ前のコミットに入れ忘れた一行を足したい。今の Git でこれをやろうとすると、手はこう動きます — git commit --fixup で fixup コミットを作り、git rebase --autosquash で畳み込み、その上にブランチを積んでいるなら --update-refs がちゃんと効いたか確認する。コミットを1つ2つに割るのはもっと大変です。インタラクティブ rebase を立ち上げ、インストラクションシートで edit に変え、git reset HEAD~ でコミットを解いて、2回に分けてステージし直して2回コミットし、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 時点で、現在のサブコマンドは3つです。

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 — コミットをインタラクティブに2つへ分割する。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 でよくやる作業(コミット順の入れ替え、コミットの分割、コミットの破棄、複数コミットの統合、tip ではないコミットの修正)を並べたあと、こう書きます。

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 と実際に何が違うか

ドキュメントが挙げる違いは3つで、一つずつ見る価値があります。

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 にまとめ、最後に一度だけコミットします。スタックブランチが5つ掛かっていれば、5つ全部が動くか、1つも動かないかのどちらかです。中途半端に壊れた状態は残りません。

--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   (merge 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 の結果コミットが空になってしまうケースは2つあり — ステージした変更が対象コミットの変更をちょうど相殺してしまう、あるいは子孫コミットがついさっき祖先へ押し込んだのと同じ変更をすでに含んでいた場合 — デフォルトの drop は空コミットを消し、keep は残し、abort はエラーで止まります。ただし、ルートコミットを drop することはまだできません。ソースの 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 Contributor Summit で議論された方向性と正確に一致するからです。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 の立場がこう記録されています — sequencer を超えていきたい、sequencer はワーキングツリーを更新しすぎて望ましくない、git historygit replay を使う方向へ進める。当時のノートには「とりあえず sequencer の underpinning のまま実験版をランディングする」という案も書かれており、結論は「UI を先にランディングし、土台は後から反復する」でした。

実際に配布されたコードを確認すると、答えが出ます。v2.55.0 の builtin/history.csequencer.h を include してはいますが、そこから使っているのはコミットメッセージ整形ヘルパーの cleanup_message() 一つだけです。rebase の sequencer 機構は呼び出しません。書き換えは replay_revisions() が、マージは merge_incore_nonrecursive() が担います。つまり、サミットで議論された「sequencer ではなく replay」という方向で、すでに作られて出てきているということです。

これはユーザーにとって実質的な違いとして返ってきます。sequencer ベースだったなら、ワーキングツリーのチェックアウトと状態ファイル(.git/rebase-merge/ のような)が伴っていたはずで、ベアリポジトリでの動作は不可能だったでしょう。

正直な限界 — これはまだ実験だ

ここからが実際に重要な部分です。ドキュメントは LIMITATIONS セクションを別に設けており、その内容は軽くありません。

マージコミットがあると使えない。 原文は This command does not (yet) work with histories that contain merges. 代わりに git rebase --rebase-merges を使うよう案内しています。ソースには、対象コミットの子孫のうち親が2つ以上あるコミットがないか事前に revwalk で確認し、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 が追加されたときにフックが壊れた前例が言及され、「それをまた繰り返したくない」という話が出てきます。今のところは一切動かさない方を選んだ状態で、ドキュメントは「将来変わる可能性がある」と含みを残しています。

ルートコミットの drop 不可。 --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.

「2番目の項目」はコンフリクト時の中断を指します。つまり、このコマンドの有用性が制限されていることを著者自身も分かっていて、その解決策として first-class conflicts を挙げています。git history のドキュメントも同じものを指しています。

first-class conflicts とは、コンフリクトを「解決するまで抱え続ける暫定状態」ではなく コミット可能な一級オブジェクト として扱うモデルです。jj はこう動きます — コンフリクトした状態をそのままコミットし、その上で作業を続け、あとから部分的に解くことができます。そうなれば fixup はコンフリクトに出会っても中断する必要がなくなります。コンフリクトを記録して先へ進めばいいからです。

では、これが Git に入る見込みはあるのか。2025年の Contributor Summit にこのテーマがセッションとして上がり、進行役は 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 が加わりました。ワーキングツリーに触れず、子孫ブランチを原子的に動かし、土台は sequencer ではなく git replay + インコアの merge-ort で組まれています。

同時に、これは正直なところ未完成です。マージも扱えず、コンフリクトが起きれば止まり、フックも動きません。そして、その限界がどこで解けるのかも、ドキュメントとコミットメッセージにすでに書かれています — first-class conflicts です。その議論をサミットで主導したのが jj を作った Martin von Zweigbergk であったこと、そして導入コミットが動機として Jujutsu を直接名指ししたこと。これが、今 Git で起きていることを要約しています。競合 VCS が先に証明したワークフローが、上流へと逆流している最中なのです。

だから今の git history は「完成した機能」というより ランディングされた問い に近いものです。UI を先に出して反応を見ようというサミットの結論そのままです。使ってみて不便な場所があれば、それはまだメーリングリストで決まっている最中の答えかもしれません。

参考資料

현재 단락 (1/114)

3つ前のコミットに入れ忘れた一行を足したい。今の Git でこれをやろうとすると、手はこう動きます — `git commit --fixup` で fixup コミットを作り、`git rebase...

작성 글자: 0원문 글자: 12,581작성 단락: 0/114