現在のブランチにコミットされていない変更がある場合に別のブランチをチェックアウトする 質問する

現在のブランチにコミットされていない変更がある場合に別のブランチをチェックアウトする 質問する

ほとんどの場合、別の既存のブランチをチェックアウトしようとすると、現在のブランチにコミットされていない変更があると Git はそれを許可しません。そのため、まずそれらの変更をコミットするか、スタッシュする必要があります。

ただし、Git では、変更をコミットまたは保存せずに別のブランチをチェックアウトすることが時々許可され、その変更はチェックアウトしたブランチに引き継がれます。

ここでのルールは何ですか? 変更がステージングされているかステージングされていないかは重要ですか? 変更を別のブランチに持ち込むことは私には意味がわかりません。なぜ Git は時々それを許可するのでしょうか? つまり、状況によっては役立つのでしょうか?

ベストアンサー1

予備的注釈

この回答は、Git がなぜそのように動作するのかを説明する試みです。特定のワークフローに従うことを推奨するものではありません。(私自身は、とにかくコミットして、git stashあまりトリッキーにならないようにすることを好みますが、他の方法を好む人もいます。)

ここでの観察は、作業を開始した後branch1(最初に別のブランチに切り替えるのが良いことを忘れていたか、気づいていなかったbranch2)、次を実行することです。

git checkout branch2

Git は時々、「OK、現在ブランチ 2 にいます!」と表示します。また、Git は「それはできません。変更の一部が失われます。」と表示します。

Git でそれができない場合は、変更をコミットして、どこかに永続的に保存する必要があります。これらを保存するために使用したい場合がありますgit stash。これは、このツールが設計された目的の 1 つです。git stash saveまたは はgit stash push実際には「すべての変更をコミットしますが、ブランチにはコミットせず、現在の場所から変更を削除します」という意味であることに注意してください。これにより、切り替えが可能になります。現在、進行中の変更はありません。git stash apply切り替え後にそれらを実行できます。

Sidebar:git stash saveは古い構文です。Gitgit stash pushバージョン 2.13 で導入され、 の引数に関するいくつかの問題を修正しgit stash、新しいオプションを使用できるようになりました。基本的な方法で使用すると、どちらも同じことを行います。

よろしければここで読むのをやめてください。

Gitで切り替えができない場合は、すでに解決策があります。git stashまたは を使用しますgit commit。変更を再作成するのが簡単な場合は、 を使用して強制的に切り替えます。この回答は、変更を開始したにもかかわらずGit で切り替えが許可される場合git checkout -fについてです。 が機能する場合機能しない場合があるのはなぜでしょうか。git checkout branch2

ここでのルールは、ある意味では単純ですが、別の意味では複雑/説明が難しいです。

作業ツリー内のコミットされていない変更を含むブランチを切り替えることができるのは、切り替えによってそれらの変更が上書きされる必要がない場合のみです。

つまり、これはまだ簡略化されていることに注意してください。段階的なgit addgit rmなどには、非常に難しいコーナーケースがいくつかありますbranch1。 にいると仮定します。A はgit checkout branch2次のようにする必要があります。

  • ありbranch1、にないすべてのファイルについてbranch2、そのファイルを削除します
  • にありbranch2にないすべてのファイルに対してbranch1、そのファイル(適切な内容)を作成します。
  • 両方のブランチにあるすべてのファイルのバージョンbranch2が異なる場合は、作業ツリーのバージョンを更新します。

これらの各ステップにより、作業ツリー内の何かが破壊される可能性があります。

  • 作業ツリー内のバージョンがコミットされたバージョンと同じであればbranch1、ファイルの削除は「安全」です。変更を加えた場合は「安全ではありません」。
  • branch2ファイルが現在存在しない場合、そのファイルをそのまま作成することは「安全」です。2ファイルが現在存在しているが、内容が「間違っている」場合は「安全ではない」です。
  • もちろん、作業ツリーのバージョンがすでに にコミットされている場合、ファイルの作業ツリーのバージョンを別のバージョンに置き換えることは「安全」ですbranch1

新しいブランチ ( git checkout -b newbranch)を作成することは常に「安全」であると見なされます。このプロセスの一環として、作業ツリー内のファイルは追加、削除、または変更されず、インデックス/ステージング領域も変更されません。(注意: 新しいブランチの開始点を変更せずに新しいブランチを作成する場合は安全ですが、別の引数 ( など) を追加すると、git checkout -b newbranch different-start-pointに移動するために変更が必要になる場合がありますdifferent-start-point。その場合、Git は通常どおりチェックアウトの安全ルールを適用します。)


1これには、ファイルがブランチにあることの意味を定義する必要があり、そのためにはブランチという単語を適切に定義する必要があります。(「ブランチ」とは具体的に何を意味するのでしょうか?) ここで、私が本当に意味しているのは、branch-name が解決されるコミットです。つまり、 がハッシュを生成する場合、パスがであるファイルは にあります。代わりにエラー メッセージが表示される場合、そのファイルは には存在しません。インデックスまたは作業ツリーにパスが存在するかどうかは、この特定の質問に答えるときには関係ありません。したがって、ここでの秘訣は、各の の結果を調べることです。ファイルが最大で 1 つのブランチに「含まれる」ため失敗するか、2 つのハッシュ ID が返されます。2 つのハッシュ ID が同じ場合、ファイルは両方のブランチで同じです。変更は必要ありません。ハッシュ ID が異なる場合、ファイルは 2 つのブランチで異なるため、ブランチを切り替えるには変更する必要があります。P branch1git rev-parse branch1:Pbranch1Pgit rev-parsebranch-name:path

ここで重要な概念は、コミット内のファイルは永久に凍結されるということです。編集するファイルは当然凍結されません。少なくとも最初は、2 つの凍結されたコミット間の不一致のみを調べます。残念ながら、私たち (または Git) は、切り替えるコミットには含まれず、切り替えるコミットに含まれるファイルも処理する必要があります。これにより、残りの複雑さが発生します。なぜなら、作業中の 2 つの特定の凍結されたコミットがなくても、ファイルはインデックスや作業ツリー内に存在する可能性があるからです。

2すでに「正しい内容」で存在している場合は、「ある程度安全」と見なされる可能性があり、Git は結局それを作成する必要がありません。少なくともいくつかのバージョンの Git ではこれが可能だったと記憶していますが、今テストしたところ、Git 1.8.5.4 では「安全ではない」と見なされることがわかりました。同じ議論は、切り替え先のブランチと一致するように変更されたファイルにも当てはまります。ただし、1.8.5.4 では「上書きされる」とだけ書かれています。技術ノートの最後も参照してください。Git をバージョン 1.5.something で初めて使い始めてから、読み取りツリーのルールは変更されていないと思うので、私の記憶が間違っている可能性があります。


変更がステージングされているかステージングされていないかは重要ですか?

branch1はい、いくつかの方法で可能です。具体的には、変更をステージングしてから、作業ツリー ファイルを「変更解除」することができます。以下は、とが異なる 2 つのブランチのファイルですbranch2

$ git show branch1:inboth
this file is in both branches
$ git show branch2:inboth
this file is in both branches
but it has more stuff in branch2 now
$ git checkout branch1
Switched to branch 'branch1'
$ echo 'but it has more stuff in branch2 now' >> inboth

この時点では、にいるにもかかわらず、作業ツリー ファイルはinbothのものと一致しています。この変更はコミット用にステージングされていないため、次のように表示されます。branch2branch1git status --short

$ git status --short
 M inboth

スペースの後に M が続くと、「変更されているがステージングされていない」ことを意味します (より正確には、作業ツリーのコピーはステージングされた/インデックスのコピーとは異なります)。

$ git checkout branch2
error: Your local changes ...

さて、作業ツリーのコピーをステージングしましょう。これは、 のコピーとも一致することが既にわかっていますbranch2

$ git add inboth
$ git status --short
M  inboth
$ git checkout branch2
Switched to branch 'branch2'

ここでは、ステージングされたコピーと作業コピーの両方が の内容と一致したbranch2ため、チェックアウトが許可されました。

別のステップを試してみましょう:

$ git checkout branch1
Switched to branch 'branch1'
$ cat inboth
this file is in both branches

私が行った変更は、ステージング領域から失われました (チェックアウトはステージング領域を介して書き込むため)。これは、少し特殊なケースです。変更は失われていませんが、ステージングしたという事実は失われています

どちらのブランチ コピーとも異なるファイルの 3 番目のバリアントをステージングし、作業コピーを現在のブランチ バージョンと一致するように設定してみましょう。

$ echo 'staged version different from all' > inboth
$ git add inboth
$ git show branch1:inboth > inboth
$ git status --short
MM inboth

ここでの2 つのは、ステージングされたファイルがファイルMと異なり、作業ツリーのファイルがステージングされたファイルと異なることを意味します作業ツリーのバージョンは(別名) バージョンと一致します。HEADbranch1HEAD

$ git diff HEAD
$

しかし、git checkoutチェックアウトは許可されません:

$ git checkout branch2
error: Your local changes ...

branch2バージョンを作業バージョンとして設定しましょう:

$ git show branch2:inboth > inboth
$ git status --short
MM inboth
$ git diff HEAD
diff --git a/inboth b/inboth
index ecb07f7..aee20fb 100644
--- a/inboth
+++ b/inboth
@@ -1 +1,2 @@
 this file is in both branches
+but it has more stuff in branch2 now
$ git diff branch2 -- inboth
$ git checkout branch2
error: Your local changes ...

現在の作業コピーが のものと一致しているにもかかわらずbranch2、ステージングされたファイルは一致していないため、 はgit checkoutそのコピーを失い、 はgit checkout拒否されます。

技術ノート - 非常に好奇心旺盛な人向け :-)

これらすべての基盤となる実装メカニズムは、Git のインデックスです。「ステージング領域」とも呼ばれるインデックスは、次のgit addコミットを構築する場所です。インデックスは、現在のコミット、つまり現在チェックアウトしているものと一致するところから始まり、ファイルを使用するたびに、インデックス バージョンを作業ツリーにあるものに置き換えます

覚えておいてください、ワークツリーはファイルを操作する場所です。ここでは、コミットやインデックスのように Git でのみ使用できる特別な形式ではなく、通常の形式になっています。つまり、コミットからファイルを抽出し、インデックスを経由してワークツリーに進めます。変更を加えたら、git addインデックスに置きます。つまり、各ファイルには、現在のコミット、インデックス、ワークツリーの 3 つの場所があります。

を実行するとgit checkout branch2、Git は内部で の先端コミットを現在のコミットと現在のインデックスの両方にあるものと比較します。現在ある内容と一致するファイルは、Git はそのままにしておきます。すべてそのままです。両方のコミットbranch2で同じファイルも、Git はそのままにしておきますこれらは、ブランチを切り替えることができるファイルです。

このインデックスのおかげで、コミット切り替えを含む Git の大部分は比較的高速です。インデックスに実際に含まれているのは各ファイルそのものではなく、各ファイルのハッシュです。ファイル自体のコピーは、Git がblob オブジェクトと呼ぶものとしてリポジトリに保存されます。これは、コミットにファイルが格納される方法と似ています。コミットには実際にはファイルが含まれていず、Git を各ファイルのハッシュ ID に導くだけです。そのため、Git はハッシュ ID (現在は 160 ビット長の文字列) を比較して、コミットXYに同じファイルがあるかどうかを判断できます。次に、それらのハッシュ ID をインデックス内のハッシュ ID と比較することもできます。

これが、上記の奇妙なコーナーケースすべてにつながる原因です。コミットXYの両方にファイル がありpath/to/name.txt、 のインデックスエントリがありますpath/to/name.txt。3 つのハッシュがすべて一致する可能性があります。2 つが一致し、1 つが一致しない可能性があります。3 つすべてが異なる可能性があります。また、がXanother/file.txtのみにあるかYのみにあり、現在インデックスに含まれているか含まれていない可能性もあります。これらのさまざまなケースごとに、個別に検討する必要があります。X から Y に切り替えるために、Git はコミットからインデックスにファイルをコピーするか、インデックスから削除する必要がありますか。そうであれば、ファイルを作業ツリーにコピーするか、作業ツリーから削除する必要もあります。その場合、インデックスと作業ツリーのバージョンは、コミットされたバージョンの少なくとも 1 つと一致している必要があります。一致しないと、Git は一部のデータを上書きすることになります。

(これらすべての完全なルールは、git checkoutあなたが期待するようなドキュメントではなく、ドキュメントgit read-treeの「2つのツリーのマージ」というセクション

おすすめ記事