開発者は常に Git を使用しています。
「うーん、私は今何をしたの?」と言ったことがありますか?
この投稿は、自信を持って歴史を書き換えるためのツールを提供します。
また、この投稿の内容をカバーするライブ トークも行いました。ビデオが好きな場合 (または読書と一緒に見たい場合) — あなたはそれを見つけることができます
Git に関する本を執筆中です。初期バージョンを読んでフィードバックを提供することに興味がありますか?メールを送ってください: [email protected]
Git で元に戻す方法を理解する前に、Git で変更を記録する方法を理解する必要があります。すでにすべての用語を知っている場合は、この部分をスキップしてください。
Git をファイルシステムのスナップショットを適時に記録するためのシステムと考えると非常に便利です。 Git リポジトリを考えると、3 つの「状態」または「ツリー」があります。
通常、ソース コードで作業するときは、作業ディレクトリから作業します。作業ディレクトリ (ectrory) (または作業ツリー) は、リポジトリが関連付けられているファイル システム内の任意のディレクトリです。
プロジェクトのフォルダーとファイル、および.git
というディレクトリが含まれています。 .git
フォルダーの内容については、
いくつかの変更を行った後、それらをリポジトリに記録することをお勧めします。リポジトリ(要するに: repo ) はcommitsのコレクションであり、それぞれのコミットは、自分のマシン上か他の誰かのマシン上にあるかにかかわらず、過去の日付でプロジェクトの作業ツリーがどのように見えたかのアーカイブです。
リポジトリには、 HEAD
やブランチなど、コード ファイル以外のものも含まれます。
その間に、インデックスまたはステージング領域があります。これら 2 つの用語は交換可能です。ブランチcheckout
と、Git は、作業ディレクトリに最後にチェックアウトされたすべてのファイルの内容と、それらが最初にチェックアウトされたときの様子をインデックスに入力します。
git commit
を使用すると、インデックスの状態に基づいてコミットが作成されます。
したがって、インデックス、またはステージング領域は、次のコミットの遊び場です。 indexで好きなことをしたり、ファイルを追加したり、何かを削除したりできます。準備ができたら、先に進んでリポジトリにコミットします。
ハンズオンの時間です🙌🏻
git init
使用して、新しいリポジトリを初期化します。 1.txt
というファイルにテキストを書き込みます。
上記の 3 つのツリー状態のうち、 1.txt
現在どこにありますか?
まだインデックスに導入されていないため、作業ツリー内。
ステージングしてインデックスに追加するには、 git add 1.txt
を使用します。
これで、 git commit
使用して変更をリポジトリにコミットできます。
作業ツリー全体を記述するツリーへのポインターを含む、新しいコミット オブジェクトを作成しました。この場合、ルート フォルダー内の1.txt
のみになります。ツリーへのポインターに加えて、コミット オブジェクトには、タイムスタンプや作成者情報などのメタデータが含まれます。
Git のオブジェクト (コミットやツリーなど) の詳細については、
(はい、「チェックアウト」、しゃれが意図されています😇)
Git は、このコミット オブジェクトの SHA-1 値も教えてくれます。私の場合、それはc49f4ba
でした (スペースを節約するために、SHA-1 値の最初の 7 文字のみです)。
このコマンドを自分のマシンで実行すると、別の作成者であるため、別の SHA-1 値が取得されます。また、別のタイムスタンプでコミットを作成します。
リポジトリを初期化すると、Git は新しいブランチ (デフォルトではmain
という名前) を作成します。とmain
ブランチしかありません。複数のブランチがある場合はどうなりますか? Git はどのブランチがアクティブなブランチであるかをどのように認識しますか?
Git にはHEAD
と呼ばれる別のポインターがあります。これは (通常) ブランチを指し、次にコミットを指します。ところで、HEAD
リポジトリにさらに変更を加える時が来ました!
さて、もう一つ作りたいと思います。それでは、新しいファイルを作成して、前と同じようにインデックスに追加しましょう。
では、 git commit
を使用します。重要なことに、 git commit
次の 2 つのことを行います。
まず、コミット オブジェクトが作成されるため、Git の内部オブジェクト データベース内に、対応する SHA-1 値を持つオブジェクトが存在します。この新しいコミット オブジェクトは、親コミットも指します。これは、 git commit
コマンドを作成したときにHEAD
が指していたコミットです。
2 番目に、 git commit
アクティブ ブランチのmain
を移動します。
履歴を書き換えるには、コミットを導入するプロセスを元に戻すことから始めましょう。そのために、非常に強力なツールであるコマンドgit reset
について学びます。
git reset --soft
つまり、前に行った最後のステップはgit commit
でした。これは実際には 2 つのことを意味します。つまり、Git は commit オブジェクトを作成し、アクティブなブランチであるmain
を移動しました。このステップを元に戻すには、コマンドgit reset --soft HEAD~1
を使用します。
構文HEAD~1
HEAD
の最初の親を参照します。コミットグラフに複数のコミットがある場合は、「コミット 2」を指している「コミット 3」と言います。これは、「コミット 1」を指しています。
そして、 HEAD
「Commit 3」を指していたとします。 HEAD~1
を使用して「コミット 2」を参照し、 HEAD~2
を「コミット 1」を参照することができます。
それでは、コマンドに戻ります: git reset --soft HEAD~1
このコマンドは、Git にHEAD
が指すものを変更するように要求します。 (注: 以下の図では、「 HEAD
が指しているすべてのもの」に*HEAD
を使用しています)。この例では、 HEAD
はmain
を指しています。したがって、Git はmain
のポインターをHEAD~1
を指すように変更するだけです。つまり、 main
「コミット 1」を指します。
ただし、このコマンドは、インデックスまたは作業ツリーの状態には影響しませんでした。したがって、 git status
を使用するとgit commit
を実行する前と同じように、 2.txt
がステージングされていることがわかります。
git log?
HEAD
から始まり、 main
に進み、次に「Commit 1」に進みます。これは、履歴から「Commit 2」に到達できなくなったことを意味することに注意してください。
「コミット2」のコミットオブジェクトが削除されたということでしょうか? 🤔
いいえ、削除されません。オブジェクトの Git の内部オブジェクト データベース内にまだ存在します。
git push
を使用して現在の履歴をプッシュすると、Git は「コミット 2」をリモート サーバーにプッシュしませんが、コミット オブジェクトはリポジトリのローカル コピーにまだ存在します。
ここで、再度コミットします。「コミット 2.1」のコミット メッセージを使用して、この新しいオブジェクトを元の「コミット 2」と区別します。
「Commit 2」と「Commit 2.1」はなぜ違うのですか?同じコミットメッセージを使用したとしても、それらが指しているとしても1.txt
と2.txt
で構成されるルート フォルダーの)、作成された時期が異なるため、タイムスタンプは異なります。
上の図では、Git の内部オブジェクト データベースにまだ存在していることを思い出させるために、「コミット 2」を残しました。 「Commit 2」と「Commit 2.1」の両方が「Commit 1」を指すようになりましたが、「Commit 2.1」のみがHEAD
から到達可能です。
さかのぼってさらに元に戻す時が来ました。今回は、 git reset --mixed HEAD~1
を使用します (注: --mixed
はgit reset
のデフォルト スイッチです)。
このコマンドはgit reset --soft HEAD~1
と同じように開始されます。つまり、 HEAD
が現在指しているポインタmain
ブランチ) を取得し、それをHEAD~1
に設定します。この例では、「コミット 1」です。
次に、Git はさらに進んで、インデックスに加えた変更を効果的に元に戻します。つまり、最初のステップで設定した後の新しいHEAD
ある現在のHEAD
と一致するようにインデックスを変更します。
git reset --mixed HEAD~1
を実行すると、 HEAD
がHEAD~1
(「コミット 1」) に設定され、Git はインデックスを「コミット 1」の状態に一致させることを意味します。 2.txt
インデックスに含まれなくなることを意味します。
元の「コミット 2」の状態で新しいコミットを作成します。今回は、作成する前に2.txt
再度ステージングする必要があります。
続けて、さらに元に戻してください!
git reset --hard HEAD~1
を実行してください。
繰り返しになりますが、Git は--soft
ステージから開始し、 HEAD
が指すもの ( main
) をHEAD~1
(「コミット 1」) に設定します。
ここまでは順調ですね。
次に、 --mixed
ステージに進み、インデックスをHEAD
と一致させます。つまり、Git は2.txt
のステージングを取り消します。
Git がさらに進んで、作業ディレクトリをインデックスのステージと一致させる--hard
ステップの時間です。この場合、作業ディレクトリからも2.txt
を削除することを意味します。
(**注: この特定のケースでは、ファイルは追跡されていないため、ファイル システムから削除されません。ただし、 git reset
を理解するためにはそれほど重要ではありません)。
Git に変更を加えるには、3 つのステップがあります。作業ディレクトリ、インデックス、またはステージング領域を変更し、それらの変更で新しいスナップショットをコミットします。これらの変更を元に戻すには:
git reset --soft
を使用すると、コミット ステップが取り消されます。
git reset --mixed
を使用すると、ステージング ステップも元に戻します。
git reset --hard
を使用すると、作業ディレクトリへの変更が元に戻されます。したがって、実際のシナリオでは、「私は Git が大好きです」とファイル ( love.txt
) に書き込みます。先に進み、これもステージングしてコミットします。
おっと!
実は、私はあなたにそれをコミットして欲しくありませんでした。
私が実際にあなたにして欲しかったのは、このファイルをコミットする前に、このファイルにいくつかの愛の言葉を書くことです.
あなたは何ができますか?
これを克服する 1 つの方法は、 git reset --mixed HEAD~1
を使用して、実行したコミットとステージングの両方のアクションを効果的に元に戻すことです。
したがってmain
再び「コミット 1」を指し示し、 love.txt
もはやインデックスの一部ではありません。ただし、ファイルは作業ディレクトリに残ります。先に進み、さらにコンテンツを追加できます。
ファイルをステージングしてコミットします。
よくやった👏🏻
「Commit 1」を指している「Commit 2.4」のこの明確で素晴らしい歴史があります。
ツールボックスに新しいツールgit reset
追加されました 💪🏻
このツールは非常に便利で、ほとんど何でも実現できます。常に最も便利なツールというわけではありませんが、慎重に使用すれば、ほぼすべての書き換え履歴シナリオを解決できます。
初心者の場合、Git で元に戻したいときはほぼいつでもgit reset
のみを使用することをお勧めします。慣れてきたら、他のツールに移ります。
別のケースを考えてみましょう。
new.txt
という新しいファイルを作成します。ステージングとコミット:
おっと。実はそれは間違いです。あなたはmain
にいましたが、機能ブランチでこのコミットを作成してほしいと思いました。私の悪い😇
この記事からあなたに取ってもらいたい最も重要なツールが 2 つあります。 2 つ目はgit reset
です。最初の、そしてはるかに重要なことは、現在の状態とあなたがなりたい状態をホワイトボードに載せることです.
このシナリオでは、現在の状態と目的の状態は次のようになります。
次の 3 つの変更点に気付くでしょう。
main
現在の状態では「コミット 3」(青い部分) を指していますが、目的の状態では「コミット 2.4」を指しています。
feature
ブランチは現在の状態には存在しませんが、存在し、目的の状態の「コミット 3」を指しています。
HEAD
現在の状態ではmain
を指し、目的の状態ではfeature
を指します。
これを描くことができ、 git reset
使い方を知っていれば、間違いなくこの状況から抜け出すことができます。
繰り返しますが、最も重要なことは、息を吸ってこれを引き出すことです.
上の図を見て、どうすれば現在の状態から望ましい状態になるのでしょうか?
もちろん、いくつかの異なる方法がありますが、シナリオごとに 1 つのオプションのみを提示します。他のオプションも自由に試してみてください。
git reset --soft HEAD~1
を使用して開始できます。これにより、 main
前のコミット「Commit 2.4」を指すように設定されます。
current-vs-desired の図をもう一度見てみると、新しいブランチが必要であることがわかりますよね? git switch -c feature
またはgit checkout -b feature
(同じことを行います) を使用できます。
このコマンドは、新しいブランチを指すようにHEAD
も更新します。
git reset --soft
を使用したため、インデックスを変更していないため、現在、コミットしたい状態とまったく同じです — なんと便利なことでしょう! feature
ブランチにコミットするだけです。
そして、あなたは望ましい状態になりました🎉
あなたの知識を他のケースに適用する準備はできましたか?
love.txt
にいくつかの変更を加え、さらにcool.txt
という名前の新しいファイルを作成します。それらをステージングしてコミットします。
ああ、おっと、実際には、変更ごとに 1 つずつ、2 つの別々のコミットを作成してほしかった 🤦🏻
これを自分で試してみませんか?
コミットとステージングの手順を元に戻すことができます。
このコマンドを実行すると、インデックスにはこれら 2 つの変更が含まれなくなりますが、両方ともファイル システムに残ります。したがって、 love.txt
のみをステージングする場合は、個別にコミットしてから、 cool.txt
に対して同じことを行うことができます。
ナイス😎
テキストを含む新しいファイル ( new_file.txt
) を作成し、テキストをlove.txt
に追加します。両方の変更をステージングし、コミットします。
おっと🙈🙈
今回は、新しいブランチではなく、既存のブランチではなく、別のブランチに配置したいと考えました。
それで、あなたは何ができますか?
ヒントをあげます。答えは本当に短く、とても簡単です。最初に何をしますか?
いいえ、 reset
しません。私たちは描く。他のすべてがずっと簡単になるので、それが最初に行うことです。これが現在の状態です。
そして望ましい状態?
現在の状態から目的の状態にどのように移行しますか? 最も簡単なのはどれですか?
先ほどのようにgit reset
を使うのもひとつの方法ですが、別の方法も試していただきたいです。
まず、 existing
ブランチを指すようにHEAD
を移動します。
直観的に、青いコミットで導入された変更を取得し、これらの変更をexisting
ブランチの上に適用 (「コピー アンド ペースト」) する必要があります。 Git にはそのためのツールがあります。
このコミットとその親コミットの間に導入された変更を取得し、これらの変更をアクティブなブランチに適用するように Git に依頼するには、 git cherry-pick
を使用できます。このコマンドは、指定されたリビジョンで導入された変更を取得し、アクティブなコミットに適用します。
また、新しいコミット オブジェクトを作成し、この新しいオブジェクトを指すようにアクティブ ブランチを更新します。
上記の例では、作成されたコミットの SHA-1 識別子を指定しましたが、変更を適用するコミットがmain
が指しているコミットであるため、 git cherry-pick main
を使用することもできます。
しかし、これらの変更がmain
ブランチに存在することは望ましくありません。 git cherry-pick
変更をexisting
ブランチにのみ適用しました。それらをmain
からどのように削除できますか?
1 つの方法は、 main
にswitch
からgit reset --hard HEAD~1
を使用することです。
できたね! 💪🏻
git cherry-pick
実際には、指定されたコミットとその親の違いを計算し、それらをアクティブなコミットに適用することに注意してください。これは、競合が発生する可能性があるため、Git がこれらの変更を適用できない場合があることを意味しますが、それは別の投稿のトピックです。
また、ブランチによって参照されるコミットだけでなく、任意のコミットで導入された変更をcherry-pick
ように Git に依頼できることに注意してください。
新しいツールを取得したので、 git reset
とgit cherry-pick
を使用しています。
さて、別の日、別のレポ、別の問題。
コミットを作成します。
そして、それをリモート サーバーにpush
。
うーん、おっと😓…
私はちょうど何かに気づきました。そこにタイプミスがあります。 This is This is more text
This is more tezt
と書きました。おっと。では、今の大きな問題は何ですか?私は ed push
。つまり、他の誰かがすでにそれらの変更をpull
ed している可能性があります。
これまで行ってきたように、 git reset
を使用してこれらの変更をオーバーライドすると、異なる履歴が作成され、すべてが崩壊する可能性があります。 push
するまでは、自分のレポのコピーを好きなだけ書き換えることができます。
変更をpush
したら、履歴を書き換える場合は、他の誰もそれらの変更を取得していないことを十分に確認する必要があります。
または、 git revert
という別のツールを使用することもできます。このコマンドは、提供しているコミットを取得し、 git cherry-pick
と同様に、その親コミットから Diff を計算しますが、今回は逆の変更を計算します。
したがって、指定されたコミットで行を追加した場合、その逆は行を削除し、その逆も同様です。
git revert
新しいコミット オブジェクトを作成しました。これは、履歴への追加であることを意味します。 git revert
使用することで、履歴を書き換えませんでした。あなたは過去の過ちを認めました。このコミットは、あなたが間違いを犯し、それを修正したことを認めたものです。
それがより成熟した方法だと言う人もいます。 git reset
を使用して以前のコミットを書き換えた場合に得られるほどクリーンな履歴ではないと言う人もいます。しかし、これは歴史の書き換えを避ける方法です。
タイプミスを修正して、再度コミットできます。
あなたのツールボックスには、新しい輝かしいツールrevert
がロードされました:
作業を完了し、コードを記述して、 love.txt
に追加します。この変更をステージングし、コミットします。
私は自分のマシンで同じことを行い、キーボードの上Up
キーを使用して前のコマンドにスクロールして戻り、 Enter
を押して…うわー。
おっと。
git reset --hard
を使用しただけですか? 😨
実際に何が起こったのですか? Git はポインターをHEAD~1
に移動したため、現在の履歴からは、すべての貴重な作業を含む最後のコミットに到達できません。また、Git はステージング領域からすべての変更をステージング解除し、作業ディレクトリをステージング領域の状態に一致させました。
つまり、私の仕事がなくなったこの状態にすべてが一致します。
フリークアウトタイム。怖がら。
しかし、本当に、びっくりする理由はありますか?そうではありません… 私たちはリラックスした人々です。私たちは何をしますか?さて、直観的に、コミットは本当に、本当になくなったのでしょうか?いいえ、なぜですか? Git の内部データベース内にまだ存在します。
その場所さえわかれば、このコミットを識別する SHA-1 値がわかるので、復元できます。元に戻す操作を元に戻し、このコミットにreset
こともできました。
したがって、ここで本当に必要なのは、「削除された」コミットの SHA-1 だけです。
問題は、どうすればそれを見つけることができるかということです。 git log
役に立ちますか?
まあ、そうではありません。 git log
、探しているコミットの親コミットを指すmain
を指すHEAD
に移動します。次に、 git log
親チェーンをトレースバックしますが、これには私の貴重な作業のコミットは含まれません。
ありがたいことに、Git を作成した非常に頭の良い人たちは、バックアップ プランも作成してくれました。これはreflog
と呼ばれます。
Git で作業しているときに、 git reset
使用して行うことができるHEAD
でなく、 git switch
やgit checkout
などの他のコマンドを変更するたびに、Git はエントリをreflog
に追加します。
コミットが見つかりました! 0fb929e
で始まるものです。
HEAD@{1}
という「ニックネーム」で関連付けることもできます。 Git はHEAD~1
を使用してHEAD
の最初の親を取得し、 HEAD~2
を使用してHEAD
の 2 番目の親を参照するなど、Git はHEAD@{1}
を使用してHEAD
の最初の reflog 親を参照します。前の手順でHEAD
指していた場所。
git rev-parse
にその値を表示するように依頼することもできます。
reflog
を表示する別の方法は、 git log -g
を使用することです。これは、 git log
に実際にreflog
を考慮するように要求します。
上記のように、 reflog
HEAD
と同様に、「コミット 2」を指すmain
を指しています。しかし、 reflog
内のそのエントリの親は「コミット 3」を指しています。
したがって、「コミット 3」に戻るには、 git reset --hard HEAD@{1}
(または「コミット 3」の SHA-1 値) を使用できます。
そして今、 git log
実行すると:
私たちはその日を救いました! 🎉👏🏻
このコマンドをもう一度使用するとどうなりますか? git commit --reset HEAD@{1}
を実行しましたか? Git はHEAD
を、最後のreset
前にHEAD
が指していた場所、つまり「コミット 2」に設定します。私たちは一日中続けることができます:
ツールボックスを見ると、Git でうまくいかない多くのケースを解決するのに役立つツールが満載です。
これらのツールを使用すると、Git の仕組みをよりよく理解できるようになります。具体的にはgit rebase
など、履歴を書き換えることができるツールは他にもありますが、この投稿ですでに多くのことを学んでいます。今後の投稿では、 git rebase
についても掘り下げます。
このツールボックスにリストされている 5 つのツールよりもさらに重要な最も重要なツールは、現在の状況と望ましい状況をホワイトボードで示すことです。これで私を信じてください、それはすべての状況を困難に思わせず、解決策をより明確にします.
また、この投稿の内容をカバーするライブ トークも行いました。ビデオが好きな場合 (または読書と一緒に見たい場合) — あなたはそれを見つけることができます
一般に、
Omer は、テルアビブ大学で言語学の修士号を取得しており、
ここで初公開