MarkdownとBullet Journal

いわゆるプログラマーのつぶやき

【Git】checkoutとresetの振る舞い

述べたいこと

Gitのcommit履歴を移動するコマンドとしてgit checkoutとgit resetがあるが、どの記事を見てもgit 3大ツリーとの関わり(挙動)を細かく記載したものがなかったため作成した(下記の別記事)。この記事では初心者向けにgit checkoutgit resetの基本的な違いに関して述べる。

programmingforever.hatenablog.com

コマンド機能表

git checkoutgit resetコマンド実行後に変更される、HEAD(commit履歴内の位置)、staging(ステージング or index)、作業ディレクトリ(Working directory)の一覧表を記載する。詳細はこの後に各図で説明するが、この表で理解出来る方は上で記載した別記事を推奨する(本記事は初心者向け)。

f:id:ProgrammingForEver:20210905090833j:plain f:id:ProgrammingForEver:20210905105850j:plain

誰を対象にした記事か

  • Git初心者
  • 失敗した時にcheckoutやresetを使うが今ひとつ違いが分からない
  • コマンド実行後の3大ツリーの状態が調べても中々掴めない

この記事が上記の様に感じている方々への解になれば幸いである。

git checkoutとgit resetの違い

git checkoutgit reset、さらにそれぞれfile名を付けた時の動作は一見似ているが、基本的に全く違う。

まず異なるのが作業ディレクトリへの影響で、checkoutは作業ディレクトリを破壊するが、resetは破壊しない(--hardオプションを除く)。これはfile指定した場合も同じだ。

次にcommitのHEAD移動だが、git checkoutは指定先にHEADを移動、git resetは指定先までHEADを抹消して戻る。またgit checkout files, git reset filesの様にfile指定するとHEADは動かない。

各コマンドの概要

以下、①checkout、②reset、③checkout file名、④reset file名、の各コマンドの概略を説明する

①git checkout:指定commitにHEADを移動、作業ディレクトリは破壊

自由に移動先のcommitを自由に指定出来るので、これまでの作業でcommitさえ切っていれば知りたい過去や分岐先に移動出来る。そこで新たに編集したい場合はブランチを切ればOKだ。そして用事が済めばいつでもmaster(最新commit)に戻れる。まさにプロジェクトのタイムマシンと言って過言ではない。作業ディレクトリにcommitしていないfileがあると、編集内容を失わない様にエラーとなるためcommitかstashによる保存が必要だ。

②git reset:指定commitまでcommitを抹消、作業ディレクトリは非破壊(--hardモードは破壊)

指定commitから最新commitまでを抹消して指定commitを新たな最新commitにする。checkout同様に過去に戻れる機能だがresetで消去したcommitにはもう戻れない*。過去を無理やり消し去る乱暴なタイムマシンとでも言うべきか。なおresetのdefault動作は作業ディレクトリ内のfileを変更しないが、各モードで異なるため詳細は⑦で述べる。

③git checkout files:指定commitからfileを戻す、作業ディレクトリは破壊

指定したcommitから指定fileをstagingと作業ディレクトリに戻す。作業ディレクトリに過去のfileを戻すので、編集中のfileが不要で過去のfileに戻したい時に使う

④git reset files:指定commitからfileを戻す、作業ディレクトリは非破壊

指定したcommitから指定fileをstagingだけに戻す。stagingだけに過去のfileを戻すので、編集に過去のfileを利用したい時に使う

実はgit resetして消失したcommitも「git reflogコマンド」を使えば取り戻せる。但しresetして消えたcommitはcommit履歴から浮いた状態のため、そのまま放置すると不要なcommitを自動消去するgitのクローラーで消されるため、戻す場合は早めに処理した方が良い

以上述べた各動作を順番に説明する。

説明

全体フロー図

git checkout, git resetの概略を理解するために、各コマンドによるfileの動きをまとめた表を掲載する。なお要素が多いため、正確な詳細を知りたい場合は上記の別記事を参照して欲しい。

f:id:ProgrammingForEver:20210901064626j:plain

commit直後の状態

まず最初に、git checkout, git resetコマンドを実行する前に、編集作業を終えてcommitを行った直後の図を示す。

f:id:ProgrammingForEver:20210830144115j:plain

file編集、staging(ステージング or index)、commit(コミット)の各作業を行い、3大ツリー(作業ディレクトリ、staging、commit:HEAD)の内容が全て一致している状態(gitの管理対象外fileを除く)。前回のcommitである変数名をhogeに変えた編集を維持していると仮定する。

一般的な編集&staging&commit

次に順調に修正を行ってからstaging(git add . コマンド)、commitをするパターンの図を見る。 f:id:ProgrammingForEver:20210830144640j:plain

$ # コマンド
$ vi hoge.sh                  # ①file編集
$ git add hoge.sh             # ②hoge.shをstaging
$ vi hoge.sh                  #  再びfile編集
$ -----
$ git commit                  # ③commit作業

図の様にfile編集後にstagingはするがcommitはせず、再度file編集している(変数名をpiyoに変えた)。これで最新commitで保存されたfile内の変数名はhoge、stagingされたfile内の変数名はfuga、作業ディレクトリのfileの変数名はpiyoとそれぞれ異なるコードになっている。この作業の後、やっぱりfugaのままで良ければそのまま③のcommitを行うし、piyoにするなら②再度stagingした上で③commitする。

④stagingから作業ディレクトリに戻す

$ # コマンド
$ git checkout              # 何も起きない(省略時はHEADで同じなので移動しない)
$ git checkout files        # stagingにある指定fileを作業ディレクトリに戻す

以下の図では、stagingの後に再度file編集したが(fuga→piyo)、失敗と判断して一旦stagingした内容を戻すべく、④stagingのfileを作業ディレクトリに戻すコマンドとして使っている。

f:id:ProgrammingForEver:20210901064755j:plain

通常はcheckoutコマンドにcommit指定が入ると、指定commitの指定fileがstagingと作業ディレクトリに戻るが、(commit指定が無いのでcommitからは戻せず)stagingから戻すだけの動作になる。

checkoutを実行すると、指定commitの指定fileをまずstagingへ戻し(1)、次に作業ディレクトリにも戻す(2)と考えても良いだろう。この④はcommit指定がないので(1)が無効となり(2)のstagingから作業ディレクトリに戻す動作だけになると判断できる

⑤HEAD commitからstagingと作業ディレクトリに戻す

最新commit後に何らかの要因でうまくいかずfileを元に戻す。commit内容に戻す場合はgit resetコマンド、指定fileを戻す場合はgit checkout files コマンドを使う。一見似ているがどれも違う内容なので注意。

$ # コマンド
$ git checkout HEAD           # 何も起きない(現在のcommitと同じなので移動しないため)
$ git checkout HEAD files     # HEADの指定fileをstagingと作業ディレクトリに戻す
$ git reset --hard            # HEADの状態にstagingと作業ディレクトリを戻す(gitの管理対象外fileを除く)

図で示す。

f:id:ProgrammingForEver:20210901064902j:plain

図の様にHEAD(最新commit)から指定fileをstagingと作業ディレクトリに戻す。

⑥HEAD commitをstagingだけに戻す。

$ # コマンド
$ git reset                 # HEAD commitの内容にstagingに戻す
$ git reset files           # HEAD commitから指定fileをstagingに戻す

この⑥は、stagingに格納したものを取り消したい時に用いる。fileを指定すると指定fileだけが取り消される。いずれも作業ディレクトリの中身は維持されるので安全だ。図を見てみる。

f:id:ProgrammingForEver:20210901064822j:plain

比較のためにすでに説明した④⑤も残している。これで④⑤⑥の振る舞いの違いが分かると思う。

reset 3モード

⑦指定commitにHEADを戻す(git reset SHA1 --soft)
⑧指定commitにHEADを戻し、file一式をstagingに戻す(git reset SHA1 (--mixed))
⑨指定commitにHEADを戻し、file一式をstagingと作業ディレクトリに戻す(git reset SHA1 --hard)

最新commitから指定commitまでの過去を抹消するgit resetコマンドは、オプションで違うモードになる。それぞれ利用目的が異なり、オプション無しの時は⑧のmixedが選ばれる。

$ # ---- resetの各モード
$ # ---- 全てHEADを指定commitまで戻すのは同じ
$ git reset SHA1 --soft           # stagingと作業ディレクトリは変化しない
$ git reset SHA1 (--mixed)        # 指定commitの内容にstagingに戻す。作業ディレクトリは変化しない
$ git reset SHA1 --hard           # 指定commitの内容にstagingと作業ディレクトリに戻す(gitの管理対象外fileを除く)

f:id:ProgrammingForEver:20210901065005j:plain

  • 各モードの利用パターン
    • ⑦はcommitを消去して、commitを無かったことにしたい時
    • ⑧はcommitを消去して、整理されたcommitに改変したい時
    • ⑨はcommitも変更fileも消去して、一からやり直したい時

⑦はcommitの記録を失うものの、stagingと作業ディレクトリはそのままなので、消去したい過去commitを好きなだけ消去した後にcommitすると最新の作業ディレクトリ内のfileに基づく最新commitが記録されて完了する。例えば作業着手から10程度のcommitを立てながら仕様変更対応が完了して動作するプログラムが作業ディレクトリにあり、バグfixで色々散らかしてしまいレビューに役立たないcommit群になっているとしよう。完成したコードを最新commitするが、着手からの過程は悪戦苦闘で無意味なcommitが連なっており、着手から現在までのcommitを無かったことにしたい場合に⑦は適している

⑧は最も価値のある使い方だ。使用例として、上記同様に作業着手から10程度のcommitを立てながら仕様変更対応が完了して動作するプログラムが作業ディレクトリにあり、同様に役立たないcommit群になっているとしよう。その散らかったcommitを今回の仕様変更の作業開始点まで戻し(reset)、実装した変更要素毎に分割staging & commitをするのに⑧は適している。stagingの中身は戻した過去commitの内容に戻っているので、git add -pコマンドで、最新の作業ディレクトリ内にある各fileの変更された全てのハンク(行の塊)が表示される。順に出てくる各ハンク毎に対話方式で、ステージングする/しないを選択する作業が行えるので容易に分割staging & commitが行える。

⑨は現在から指定commitまでの過去の間に構築したものを全て破棄する。つまり着手から現在までのcommitや変更fileを全て抹消したい場合に⑨は適している。これまで積み重ねた作業を全て失うアクションであり要注意(しばらくの間はgit reflogで取り戻せる)。

いずれも存在するcommitが消失するため共同開発で公開されたcommitに対しての利用は避けるべき。

⑩指定commitに移動/指定commit内の指定fileを戻す

git checkoutコマンドは安全に過去の指定commitに移動してファイルを閲覧出来るコマンド。また-bオプションで指定commitに新規ブランチを立てることも出来る。なお、git checkout filesとfile指定があると指定commitから指定fileをstagingと作業ディレクトリに戻す、全く異なる動作になる点に注意。またgit reset filesとresetにfile指定があると指定commitから指定fileをstagingだけに戻す動作になる。

$ # コマンド
$ git checkout SHA1            # HEADを指定commitに移動し、指定commitのfile一式をstagingと作業ディレクトリに戻す
$ git checkout SHA1 files      # 指定commitから指定fileをstagingと作業ディレクトリにコピー、HEADは変わらない
$ git reset SHA1 files         # 指定commitから指定fileをstagingだけにコピー、HEADは変わらない
  • 過去のcommitに移動して中身を精査したい時やブランチを切る用途にはfile名を付けない。なお作業ディレクトリのfileが変更されていたら移動するとfileを失うためエラーで停止する。エラーを解除するにはcommitかstashで未変更fileを保存するか、 -f オプションで強制して(fileを失う)再度checkoutする。精査後はgit checkout masterコマンドで元の最新commitに戻れる。
  • stagingと作業ディレクトリに、過去のcommitのfileを戻したい(*同名fileは過去のfileに置き換わる)場合は、git checkout filesの様にcheckoutコマンドの末尾に「.」やfile名を付ける。HEADの位置は変わらない。作業ディレクトリのfileが変更されていても構わず実行されるため、同一名のfileがあればその変更内容を失う。
  • stagingだけに、過去のcommitのfileを戻したい(*同名fileは過去のfileに置き換わる)場合は、git reset filesの様にresetコマンドの末尾に「.」やfile名を付ける。HEADの位置は変わらない。作業ディレクトリは変更されないので変更内容を失わない。

f:id:ProgrammingForEver:20210901065032j:plain

checkoutコマンドは、自由に選択出来る指定commit内に格納された特定fileまたはfile一式をstagingと作業ディレクトリに戻す。よって、

  • SHA1(ハッシュ)やHEAD, HEAD^ ,HEAD~4などの指定commitから指定fileをstagingと作業ディレクトリに戻す
  • ⑤HEAD commitから指定fileをstagingと作業ディレクトリに戻す(上記の指定がHEADになっただけ)
  • ④commit指定が無く、staging内容から指定fileを作業ディレクトリに戻す

といずれの場合も「指定commit」から希望するfileをstagingと作業ディレクトリに戻す点は同じだ。

どう使い分けるか

git resetの3モード、checkout、checkout files、reset filesをどう使い分けるか。人によって異なるのは当然だが、だいたい下記の様な使い分けになるだろう

  • git reset --softは、commitを無かったことにしたい時
  • git reset --mixedは、整理されたcommitに改変したい時
  • git reset --hardは、一からやり直したい時
  • git checkoutは、以前のVer.確認やブランチを切りたい時
  • git checkout files は、以前のVer.のfileに置き換えたい時
  • git reset files は、以前のVer.のfileを利用したい時