LoginSignup
6
1

More than 1 year has passed since last update.

git rebase解説

Posted at

以下は社内向け勉強会のLT枠で話した内容をベースにして編集増補したものである。増補しただけでなくそもそも1回分を記事にしたものではなかったりもするので、この内容を5分で話したわけではない。

git rebaseコマンドの概要

git rebaseはあるコミットに続けて、履歴にない別の一連のコミットを適用するためのコマンドである。

git mergeでできる、別ブランチの修正ファイルの取り込みはgit rebaseでも実施可能である。git mergeでの取り込みはマージコミット、つまり親を複数持つコミットによって親方向へもコミット履歴の分岐が発生するが、git rebaseでは親方向へはコミット履歴の分岐は一部のオプションによるもの等を除き発生しないという違いがあり、プロジェクトごとに取り込みに際しgit mergeとgit rebaseとどちらかだけを使用すると決まっていることが多い。

ただ、git rebaseによる取り込みはgit mergeによるものより実行者の負担が多いことは留意すべきである。これは主にgit mergeは取り込み時に競合の解決が最大1回で済むのに対して、git rebaseでは競合の解決は1回で済まないことが良くあるのが原因である。競合の解決作業はそれなりに気を使ってやらないければならないし、それなりの時間も要する。

一方でgit rebaseは別ブランチの修正ファイルの取り込み以外にもgit mergeではできないような、例えば複数のコミットをまとめてしまうのにも使用できるので、より多くの用途に使用できるコマンドであると言える。

いずれの場合であってもgit rebaseによる処理は良く「歴史の改変」と言われる。これはgit rebaseを行うと通常、コミットは差分が同じ新しいコミットに置き換えられ、ブランチのコミット履歴が修正されるからである。ブランチのコミット履歴が修正されたものを、別のユーザーも作業に利用するリモートブランチにプッシュし直すとその後のリモートからのプルが大変な手間になる公算が大きい。そのため、そうしなければならない理由がある時を除けば別のユーザーも作業に利用するリモートブランチにプッシュし直してコミット履歴を修正すべきではない。

git rebaseコマンドの形

git rebaseコマンドは、git mergeコマンドと同様に、これからリベースを開始する時に実行するコマンドと、競合等のためにリベースが中断している時に実行するコマンドとの2つに分かれる。

これからリベースを開始する時のコマンドの基本の形はgit rebase [オプション] [--onto <リベース開始点commit-ish>] [<適用対象外commit-ish> [<リベース対象ブランチ>]]である。commit-ishはコミット及びコミットを参照しているもののことである。

上記コマンドの[と]で囲まれた部分は省略可能、という意味である、つまり「--onto <リベース開始点commit-ish>」、適用対象外commit-ish、リベース対象ブランチはいずれも省略可能であるが、適用対象外commit-ishを省略し対象ブランチを省略しないことはできない(一部のオプションを指定した場合を除く)。

適用対象外commit-ishのデフォルト値はupstreamとして記録されているブランチ(通常はリモート追跡ブランチ)の指すコミットである。もしupstreamが記録されておらず、かつ適用対象外commit-ishを省略した場合はリベース開始前にエラー終了する。

リベース対象ブランチが指定されていない場合はgit rebase実行前のHEAD、つまり作業中のブランチがデフォルト値である。

「--onto <リベース開始点commit-ish>」が指定されていない場合は、デフォルト値は「適用対象外commit-ish」となる。

競合等でリベースが中断している時のためのコマンドはgit rebase <オプション>でオプション以外の引数を指定できない。ここで指定できるオプションはgit mergeにも存在する--continue, --abort, --quitの3種類に加え、--skip, --edit-todo, --show-current-patchの合わせて6種類のうち1つであり、他のオプションは指定できない。

これからリベースを開始する時のコマンドの動き

この開始する時の基本の形を追加オプションなし、git rebase [--onto <リベース開始点commit-ish>] [<適用対象外commit-ish> [<リベース対象ブランチ>]]で実行すると、以下の処理が順に行われることになる。

  • リベース開始点commit-ishへ作業ブランチを切り替え
  • リベース対象ブランチの履歴からコミットを1つ1つ適用(ただし、適用対象外commit-ishの履歴に含まれるコミットは対象外)
  • リベース対象ブランチへ作業ブランチを切り替え、ブランチの指すコミットを適用後の最新コミットにする

以降、上から順に説明する。

リベース開始点commit-ishへ作業ブランチを切り替え

まず、最初にHEADが参照するブランチ、つまり作業中のブランチをリベース開始点commit-ishに切り替える。つまり処理的にはgit checkout <リベース開始点commit-ish>と同じである。この切り替えによってdetached HEADになるが、後述するgit rebase --quitでリベースを終了するのでない限り一時的なものである。

--ontoオプションでリベース開始点commit-ishが指定されていない場合はデフォルト値は適用対象外commit-ishなので、適用対象外commit-ishに切り替える。git rebaseを実行する時に最も良く使われる形はgit rebase <取り込みたいブランチ>だと思うが、基本形git rebase [--onto <リベース開始点commit-ish>] [<適用対象外commit-ish> [<リベース対象ブランチ>]]において「取り込みたいブランチ」に当たるのは「適用対象外commit-ish」である。取り込みたいのに「適用対象外commit-ish」とはどういうことか、と思うかもしれないが、つまり最初に適用対象外commit-ishに移動することで取り込んでいるのである。

なお、man git-rebaseにはこの時切り替え先となるのは「リベース対象ブランチ」と書かれているが、冗長な出力を行う-vオプションで確認すると「リベース開始点commit-ish」の方である。「リベース対象ブランチ」へは後の工程で切り替えられるので、記述の誤りというよりは説明上の都合であろう。

リベース対象ブランチの履歴からコミットを1つ1つ適用

次にリベース対象ブランチの履歴に含まれるすべてのコミットを1つ1つ適用することになる。

まず、リベース開始点commit-ishがリベース対象ブランチの履歴上の先祖であり、かつ-fオプション(あるいは--force-rebase、またはgit mergeでも同様の効果をもたらすオプションである--no-ff)を指定していない場合、コミットを1つ1つ適用する代わりにfast-forwardで取り込むことになる。つまり、HEADが指すコミット(HEADがブランチを指しているならそのブランチが指すコミット)をリベース対象ブランチの指すコミットに変更し、次の工程であるリベース対象ブランチへ作業ブランチを切り替え、ブランチの指すコミットを適用後の最新コミットにするに進む。

fast-forwardで取り込まない場合、リベース対象ブランチの履歴に含まれるすべてのコミットのそれぞれが持つ、親コミットとの差分を1つ1つ適用していく。ただし、適用対象外commit-ishの履歴に含まれるコミットは適用対象とはならない。処理としてはgit cherry-pick <適用対象のコミット1つ>を繰り返し実行することに相当する。これで適用されたコミットはオブジェクトID(コミット毎に付く16進数40桁で表されるID)が適用前のコミットとは違うものになる。

適用対象外commit-ishの履歴に含まれるコミットは適用対象とはならないと書いたが、これの判断基準は「ファイルの内容の差分が同じかどうか」である。差分が同じかどうかは「パッチID」と呼ばれるもので判断される。あるコミットのパッチIDはgit show <コミットのオブジェクトID> | git patch-id | cut -d ' ' -f 1で取得することができ、同じパッチIDならば差分が同じという扱いとなる。なお、差分が同じコミットが複数回現れた場合、これらのコミットはすべて適用対象外である。

ただし、--reapply-cherry-picksオプション指定時は差分が同じかどうかでの適用対象外判定は行われず、履歴に共通して存在するコミット以外は適用対象となる。

適用元のコミットがマージコミットの場合、-rオプション(あるいは--rebase-merges。昔は近い効果の-pオプションもあったが今はなくなった)を付けていれば新たなマージコミット、つまり親が複数あるコミットになって適用されるが、-rがなければ無視される。ただし、-rオプションがあってもファイル内容は無視された時と同じになるので、マージ時に競合の解決等で修正したものは消えてしまう。

適用が行われた後、HEADが指すコミット(HEADがブランチを指しているならそのブランチが指すコミット)は適用されたコミットに変更される。この処理は適用1回ごとに行われる。

git mergeと同じく、この適用を行う時にすでに同じファイルの同じような箇所が修正されている場合は競合が発生する。競合が発生した場合はその時点で一旦リベースは中断され、手作業での修正を待つことになる。

競合の解決

競合が発生した場合、「CONFLICT (add/add): Merge conflict in <競合が発生したファイル名>」のようなメッセージが出力されてリベースが中断する。この状態でgit statusを実行すると以下のように出力され、競合しているファイルはUnmerged pathsの後にリストアップされる。この場合は「e」というファイルが競合している。

interactive rebase in progress; onto 1be8765
Last command done (1 command done):
   pick 9d28d10 1つ目のコミット
Next commands to do (4 remaining commands):
   pick c74589d 2つ目のコミット
   pick 7b04737 3つ目のコミット
  (use "git rebase --edit-todo" to view and edit)
You are currently rebasing branch 'b5' on '1be9995'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
        both added:      e

競合を解決するには、競合したファイルをあるべき内容に修正してgit addする。git addするところまではgit mergeと何も変わらない。つまり、

  • 競合しているファイルをエディタ等で開く。「<<<<<<< HEAD」という行、「=======」という行、「>>>>>>> (適用されるコミット)」というマーカー行ができている。「<<<<<<< HEAD」という行と「=======」という行の間が取り込む側にあった内容、「=======」という行と「>>>>>>> (適用されるコミット)」という行の間が取り込まれる側にあった内容である。
  • 適切な内容に修正し、マーカー行「<<<<<<< HEAD」「=======」「>>>>>>> (適用されるコミット)」も消す。
  • 競合箇所はファイル内に複数あるかもしれないのであれば全部解決する。
  • ファイルの中身があるべき内容になったらそのファイルをgit addしてインデックスに配置する。

となる。git mergeと違うのはgit commitでは次に進まないところで、次に進めるにはgit rebase --continueを使用する。

また、git rebaseでは適用されるコミット1つごとに競合しているかのチェックが行われているので、git rebase --continueを実行してもまたすぐにリベースの中断が発生する可能性がある。そのような事態になるとかなりの手間になるので、可能ならgit rebaseは適用が小さい範囲に収まる時に実行した方が良い。

--continue以外のリベースの中断中に使用できるオプションはリベースの中断にて説明する。

リベース対象ブランチへ作業ブランチを切り替え、ブランチの指すコミットを適用後の最新コミットにする

適用中に一度も中断されなかった、あるいは中断があってもgit rebase --continue等で中断をすべて完了した場合、HEADが参照するブランチ、つまり作業ブランチをリベース対象ブランチに切り替える。つまり処理的にはgit checkout <リベース対象ブランチ>と同じである。「リベース対象ブランチ」と書いているが実際にはcommit-ishも指定でき、ブランチでなくcommit-ishを指定した場合はdetached HEADになる。この切り替えはリベース終了後もそのままである。

さらに作業ブランチが指すコミットを上記「リベース対象ブランチの履歴からコミットを1つ1つ適用」で処理を行った後の最新コミットにする。つまり処理的にはgit reset --hard <適用後の最新commit-ish>と同じである。

これでリベースは終了である。

いくつかのリベースの例

文章だけではなかなかわかりづらいのでリベースの例を図で示す。いずれの例もマージコミットを含まないものとする。先に書いた通りマージコミットは-rオプションがない場合は適用対象外である。

以下のような状態である時に、現在作業中のブランチに別のブランチのコミットを取り込もうとする場合、git rebase <取り込まれる側ブランチ>を実行すれば良い。もし今いるブランチが下の図の「現在作業中のブランチ」でないのなら、リベース対象ブランチに下の図の「現在作業中のブランチ」を指定してgit rebase <取り込まれる側ブランチ> <下の図の「現在作業中のブランチ」>とすれば同じ結果になる。別のブランチにいたとしてもリベース対象ブランチを指定すれば同じ結果になるのはこの先の例でも同様である。

      C---D 取り込まれる側
     /
A---B       現在作業中のブランチ

これはfast-forwardで取り込まれるので実行後の状態は以下のようになる。fast-forwardは指すコミットを変更するだけなのでコミット履歴の修正は行われない。

A---B---C---D 現在作業中のブランチ & 取り込まれた側

次はfast-forwardにはならない例である。これもgit rebase <取り込まれる側ブランチ>を実行すれば取り込むことができる。

      C---D     取り込まれる側
     /
A---B---E---F   現在作業中のブランチ

結果は次のようになる。

      C---D          取り込まれた側
     /     \
A---B       E'--F'   現在作業中のブランチ

元のコミットE,Fは、差分が同じだが別のオブジェクトIDを持つコミットE',F'になって現在作業中のブランチの最後に付く。取り込まれた側はgit mergeと同じく取り込まれた後もコミットDを指したまま変わらない。

上記の例に追加して、リベースを行う前、履歴がA---B---Eだった時点でリモートに一度プッシュを行っており、リベースを行った後、A---B---C---D---E'--F'になった時点でもプッシュを行うとする(このプッシュはfast-forwardではないので-f等を付けて強制プッシュする必要がある)。しかしこのリモートブランチはすでに別のユーザーがプルしており、プルしたブランチから作ったトピックブランチにてコミットGを行っているとする。そのユーザーは新しくプッシュされたA---B---C---D---E'--F'をプルして取り込もうとする場合、以下の図のようになる。

      C---D---E'--F'     取り込まれる側
     /
A---B---E---G            現在作業中のブランチ

git rebase <取り込まれる側ブランチ>を実行すると結果は以下となる。

      C---D---E'--F'    取り込まれた側
     /             \
A---B               G'  現在作業中のブランチ

コミットEとE'とは差分が同じなので、取り込まれる側のE'の後に適用されるはずだったEは飛ばされる。結果としてこのくらいの単純な状況でのリベースは理想的な状態になる。

今度は--ontoオプションを使う場合の例である。--ontoオプションは分岐元を任意のコミットに変更する場合に使うことができる。

A---B---C---D     取り込まれる側
     \
      E---F       現在作業中のブランチ

まず説明用の単純な例として、この場合に分岐元コミットをBからCに変更したいとするならばgit rebase --onto C <取り込まれる側ブランチ>を実行すれば良い。

A---B---C---D     取り込まれる側
         \
          E'--F'  現在作業中のブランチ

現在作業中のブランチのコミット履歴(A-B-E-F)にあり、かつ取り込まれる側ブランチのコミット履歴(A-B-C-D)にないコミットが--ontoで指定したコミットの先に適用される。この例の場合、コミットEとFがCの先に適用されE'とF'となる。つまり結果として分岐元はBからCに変更されることになる。ただ、この例の場合はgit rebase --onto C <取り込まれる側ブランチ>の代わりにgit rebase --onto C Cにしても結果は同じであるし、何ならgit rebase Cでも結果は一緒である。

今度は--ontoを使ったもう少し意味のある例を示す。

A---B---C---D          新しい分岐元にしたいブランチ
     \
      \       H---I    現在作業中のブランチ
       \     /
        E---F---G      現在分岐元となっているブランチ

分岐元を「新しい分岐元にしたいブランチ」に変更する場合、git rebase --onto <新しい分岐元にしたいブランチ> <現在分岐元となっているブランチ>を実行すれば良い。現在作業中のブランチの履歴にあって現在分岐元となっているブランチの履歴にないコミットHとIが--ontoに指定した新しい分岐元にしたいブランチ、つまりDの先に適用されることになる。

A---B---C---D          新しい分岐元となったブランチ
     \       \
      \       H'--I'   現在作業中のブランチ
       \
        E---F---G      元々分岐元だったブランチ

--ontoは履歴上の一部のコミットを削除するのにも使用できる。

A---B---C---D---E---F

これにgit rebase --onto B Dを実行すると、分岐元がBで、Dまでのコミットが適用対象にならないのでEとFだけが別のオブジェクトIDを持つコミットE'、F'となり、Bの後に付く。

A---B---E'--F'

なお、この実行前の状態の時に、

A---B---C---D---E---F

git rebase Bを実行するとどうなるだろうか。分岐元がBでC以降のコミットはすべて適用対象である。これはfast-forwardで取り込めるので最終的に何も変化しない。しかし実はこれは無意味ではない。後に説明する対話式によるリベースにて2つ以上の「コミットをまとめる」のに使用できる。

対象に関するgit rebaseのオプション

以下は基本形git rebase [--onto <リベース開始点commit-ish>] [<適用対象外commit-ish> [<リベース対象ブランチ>]]で指定するには面倒だが需要がある対象をショートカット的にオプションにしたものである。

  • --root

リベース対象ブランチのコミット履歴に含まれるコミットを履歴の最初からすべて取り込み対象とする。

このオプションを指定した場合は「適用対象外commit-ish」を指定できないが、代わりに「--onto <リベース開始点commit-ish>」で指定したベース開始点commit-ishが適用対象外commit-ishの扱いとなり、リベース開始点commit-ishの履歴に含まれるコミットは取り込み対象から外れる。

  • --keep-base

--ontoオプションでリベース開始点commit-ishを指定する代わりに指定することができ、リベース開始点をコマンド実行前のHEADが「適用対象外commit-ish」から分岐した元のコミットとする。この「分岐した元のコミット」はコミット履歴を使って取得される。つまり、git merge-base <適用対象外commit-ish> <リベース対象ブランチ>の実行結果である。

  • --fork-point

適用対象外コミットを、コマンドに指定した適用対象外commit-ishではなく「リベース対象ブランチ」が「適用対象外commit-ish」から分岐した元のコミットとする。この「分岐した元のコミット」はコミット履歴から取得されるものではなくgit merge-base --fork-point <適用対象外commit-ish> <リベース対象ブランチ>の結果である、reflogを使った操作履歴によるものである。

対話式によるリベース

-i(あるいは--interactive)オプションはリベースで行われる処理のうち、リベース対象ブランチの履歴からコミットを1つ1つ適用の処理内容を編集するオプションである。

適用時の処理内容一覧は「todoリスト」と呼ばれており、.git/rebase-merge/git-rebase-todoというファイルがその実体である。ただ、todoリストを編集するのにこのファイルを直接触る必要はない。代わりに編集のためにエディタを起動してくれるオプションが-i、というわけである。

なお、todoリストは-iオプションを指定した時だけ生成されるわけではない。-iオプションがないとリベース開始時に編集を行うことができないだけである。

todoリストには行ごとに1つずつ処理が書かれ、上から順に処理される。例として-i以外のオプションなしでgit rebaseを実行した場合、以下のような「pick <適用されるコミットのオブジェクトID上位7文字> <コミットメッセージ>」が何行かとコメントが書かれたtodoリストファイルを編集するモードになる。

pick 08e6c24 1つ目のコミット
pick 1f742c3 2つ目のコミット

# Rebase 0d90033..1d81704 onto 0d90033 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

todoリストの編集

このtodoリストにある、"pick"というのは単純に適用する処理を行うコマンドである。このコマンド部分を書き換えることで(todoリストの編集なしではできないようなことも含めて)処理の内容を変更することができる。また、行を追加削除して行う処理を増減することもできる。

行を削除した場合、削除された行のコミットは適用対象にならないが、git configで設定可能なrebase.missingCommitsCheckの設定値によって行の削除時には警告やエラーとすることもできる。エラーとなった場合はリベースが中断される

また、すべての行を削除した場合もエラーとなるが、この場合は中断ではなく終了となる。-iで編集を始めた時点ではまだリベース開始点commit-ishへ作業ブランチを切り替えも行われていないので、ここで終了してしまった場合はgit rebaseの実行によって何も起こらずに終了することになる。

コマンドは"pick"の代わりに"p"でも同じ意味になる。コミットのオブジェクトIDは先頭から4文字以上指定されていればリポジトリ内で一意に定まっている場合は自動で特定してくれる。コマンドのほとんどは引数としてコミットのオブジェクトIDの指定を必要とする。以下に別のコマンドの説明も行うが、特に言及がなければコマンドには引数としてコミットのオブジェクトIDの指定が必要である。一方、コミットメッセージの部分はユーザーから見てのわかりやすさのために表示されているだけで、今ここで編集しても、あるいは消してしまったとしても何も起こらない。

コミットを適用し、その際にコミットメッセージの編集を行う場合、コマンドの部分を"reword"あるいは"r"とする。

コミットを適用して、その後にリベースを中断させる場合、コマンドの部分を"edit"あるいは"e"とする。

"break"あるいは"b"コマンドもリベースを中断させる。このコマンドは引数を取らない(なので、1行に「break」とだけ書く)。つまり"edit"との違いは「コミットを適用して、その後にリベースを中断させる」ではなく「すぐにリベースを中断させる」となることである。

コミットを適用する際にgit commit --amendのごとく前のコミットにまとめてしまうコマンドもある。"squash"あるいは"s"、"fixup"あるいは"f"コマンドである。"fixup"の方は-cあるいは-Cというオプションを採ることができる。これらの違いは採用されるコミットメッセージである。

  • fixup: 前のコミットのコミットメッセージが使用され編集することはできない。
  • fixup -C: 今のコミットのコミットメッセージが使用され編集することはできない。
  • fixup -c: 今のコミットのコミットメッセージがコメントアウトされず、前のコミットのコミットメッセージがコメントアウトされたのを初期状態として編集することができる。
  • squash: 今のコミットのコミットメッセージも前のコミットのコミットメッセージもコメントアウトされていないものを初期状態として編集することができる。

"fixup"や"squash"は他のブランチからの取り込みよりも、自分の履歴の2つ以上のコミットをまとめるのに良く使用される。これがgit rebaseの-iオプションの最も主要な利用法と言って良いくらいである。

コミットを適用しないという"drop"あるいは"d"コマンドもある。"drop"を使用した場合、上記の「行を削除した場合」と違ってgit configで設定可能なrebase.missingCommitsCheckの設定値がいずれであっても警告やエラーにならない。また、「すべての行を削除した場合」にもならないので終了する状況にもならない。

ちなみに、「すべての行を削除した場合」対策としては「何もしない」"noop"コマンドを1つだけ書いておけばまったく何もしないtodoリストができあがる。"noop"は引数を取らない。

"label"あるいは"l"というコマンドは引数として適当な名前のラベルを指定する。その指定されたラベルに現在指されているコミットを紐付ける。このラベルは"reset"あるいは"t"コマンドに続けて指定することで、指すコミットを紐付けられたものに変更するために使用することができる。

"merge"あるいは"m"というコマンドはマージコミットを作成することができる。指定の仕方はmerge -C <コミットのオブジェクトID> <ラベル>あるいはmerge -c <コミットのオブジェクトID> <ラベル>であり、指定したオブジェクトIDを持つコミットとラベルに紐付くコミットの両方を親とするマージコミットが作成される。-Cと-cの違いは、-Cを指定した場合はコミットメッセージに指定したオブジェクトIDを持つコミットのコミットメッセージが使用され、-cを指定した場合はコミットメッセージを編集することができる。なお、git rebaseに-rオプションを-iと共に指定すれば、"label"、"reset"、"merge"がtodoリストに現れているのを見ることができる。

"exec"あるいは"x"というコマンドは引数としてシェルで実行するコマンドを指定する。その指定したコマンドがエラー終了した場合はリベースが中断する。用途としてはその時点でのテストを行ったりバックアップを行ったりするためのものだろう。エラー終了したとしても"exec"行は実行後に通常todoリストから消えるが、git rebaseに--reschedule-failed-execオプションを指定した場合やgit configで設定可能なrebase.rescheduleFailedExecの設定値がtrueの場合は残ったままになる。

-iオプションで編集が開始された場合、編集が終了した時点でtodoリストの上から順に処理が開始される。

todoリストに影響を与えるgit rebaseのオプション

先に書いた通り、-iオプションがなくてもtodoリストは生成されるので、これらのtodoリストに影響を与えるオプションも-iが指定されていなくても効果がある。

  • --autosquash

todoリストにおいて、"pick"コマンドの代わりにコミットメッセージが"squash! "で始まるコミットは"squash"コマンドとなり、コミットメッセージが"fixup! "で始まるコミットは"fixup"コマンドとなり、"amend! "で始まるコミットは"fixup -C"コマンドとなる。このようなコミットは、コミットする際にコミットメッセージを手書きする以外にもそれぞれgit commit --squash HEADgit commit --fixup HEADgit commit --fixup amend:HEADで作ることができる。

git configで設定可能なrebase.autoSquashの設定値がtrueの場合、常に--autosquashが指定されているように振る舞う。この時にautoSquashを打ち消す--no-autosquashオプションもある。

  • -x <シェルで実行するコマンド>, --exec <シェルで実行するコマンド>

todoリストにおいて、すべての"pick"行や"merge"行の後に"exec <シェルで実行するコマンド>"行を追加する。--autosquashオプションによって"squash"行や"fixup"行がある場合は、その前に入るべきだった"exec"行は続く一連の"squash"行や"fixup"行の後に入る。

リベースの中断

競合やtodoリストでの指示等によってリベースが中断した場合に使用できるオプションをgit rebaseは6種類用意している。git mergeにもある--continue、--abort、--quitの3つと、git mergeにはない追加の3つ、--skip、--edit-todo、--show-current-patchである。git mergeにある3つについては大体同じような処理が行われる。

ちなみに、リベース中かどうかは.git/rebase-mergeディレクトリが存在するかどうかで判定される。

  • --continue

todoリストの次の処理に進む。競合がまだ解決されていない等の理由で取り込まれるべきファイルがワークツリーにまだ残っている場合はエラーとなる。競合が原因で取り込みによるコミットがまだ作成されておらず、現在のコミットとインデックスとの内容に差異がある場合はインデックスの内容でコミットが作られ、コミットメッセージの入力に移る。git merge --continueとは違ってgit commitgit rebase --continueの代わりにはならない。

  • --abort

リベースを終了し、git rebaseが実行される前の状態に戻す。git merge --abortとは違ってgit reset --mergegit reset --hardgit rebase --abortの代わりにはならない。

  • --quit

現状のままリベースを終了する。

  • --skip

インデックスとワークツリーを現在のコミットと同じ状態にしてからtodoリストの次の処理に進む。つまり、競合があったとしても(ユーザーが自分でgit commitによってコミットとかしてなければ)適用前の状態に戻されることによって競合がない状態になってから次の処理に進むことになる。

  • --edit-todo

todoリストの編集を行う。なお、中断の原因となったtodoリストの処理を含め、それ以前の処理はtodoリストからもうすでに消えているので、todoリストに残っているのは将来の処理のみである(例外として--reschedule-failed-execオプション指定時の"exec"コマンドがエラーになった時は消えずに残る)。-iオプションによる編集とは違い、編集終了時に自動的に処理が開始されたりはしないので、git rebase --continueを使用して処理を開始する必要がある。

  • --show-current-patch

競合が発生している場合に今適用されようとしているコミット1つの中身を見る。ちなみにこれから適用されようとしているコミットはオブジェクトIDが.git/REBASE_HEADに記録されているため、git rebase --show-current-patchは内部的にはgit show REBASE_HEADを実行している。todoリストの"edit"コマンドによって等、競合せずに中断している場合にgit rebase --show-current-patchを実行するとエラーになる。

autostash

git rebaseもgit mergeと同じようにインデックスに変更が登録されている時や、取り込もうとする変更ファイルがワークツリーで更新されている場合には実行に失敗しエラーとなる。

そしてこれもgit mergeと同様に--autostashオプションによりワークツリーの変更の一時的な退避と復元を自動的に行うことができる。行われることも一部を除いてgit mergeと特に変わりないのでgit mergeのautostashのマージをリベースに読み替えれば大体良い。

要約すると、

  • リベース開始時にgit stash save相当の処理で退避を行い、リベース終了時にgit stash pop相当の処理で退避からの戻しが行われる
  • 退避の対象となるのはワークツリーの変更だがgit statusで"Untracked files"に表示されるファイルは退避の対象外
  • git rebase --abortで終了した場合でも退避していたものを戻してくれる
    • git rebase --quitで終了した場合は自動では戻してくれないので手動でgit stash pop等で戻す必要がある(これはgit mergeとは違うところ)
  • 状況によっては退避していたものを戻す時に競合が発生するので、その場合は解決する必要がある
  • 退避していたものを戻す時に競合が発生した場合、git stash listすると退避した内容が残っているので、競合解決後にgit stash drop等で掃除した方が良い

リベース前の状態に戻す

リベース終了後にリベース前の状態に戻す場合、リベース対象ブランチを指定していたのでなければgit reflogで「rebase (start):」と書かれている1つ前に戻れば良い。つまり、「rebase (start):」と書かれているのが"HEAD@{3}"の場合、git reset --hard HEAD@{4}を実行するとリベース前の状態に戻ることができる。

リベース対象ブランチを指定していた場合はgit reflogで元の場所を探すのは簡単ではない。そのため、reflogの保存ファイルである「.git/logs/refs/heads/<リベース対象ブランチ>」を開くとgit reflogとは逆に後の方が新しいログになっており、各行は変更前のオブジェクトIDと変更後のオブジェクトIDで始まるようになっているので、一番最後の行の変更前のオブジェクトIDに戻るようにgit reset --hard <一番最後の行の変更前のオブジェクトID>とすれば戻ることができる。また、作業中のブランチが変わっているのでこれも戻すならば元のブランチは覚えていなくてもgit reflogでわかるのでgit checkoutで戻れば良い。

git rebase --quitで終了した状態から戻す場合はdetached HEADになっており、ブランチには影響がないのでgit reset --hardできれいにした後、元のブランチにgit checkoutで戻れば良い。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1