1. ホーム
  2. git

[解決済み] 現在のブランチにコミットされていない変更がある場合、別のブランチをチェックアウトする

2022-03-21 20:08:35

質問

既存の別のブランチをチェックアウトしようとすると、現在のブランチに未コミットの変更がある場合は、ほとんどの場合 Git がそれを許可してくれません。だから、まずその変更をコミットするか隠しておかなければなりません。

しかし、時折 Git はコミットや変更を保存せずに別のブランチをチェックアウトすることを許可し、私がチェックアウトしたブランチに変更を反映させることがあります。

ここでのルールは何でしょうか?変更がステージングされているか、ステージングされていないかは重要ですか?変更を別のブランチに運ぶというのは、私には何の意味もありません。なぜgitは時々それを許すのでしょうか? つまり、ある状況下では役に立つのでしょうか?

解決方法は?

予備知識

この回答は、以下の説明を試みるものです。 なぜ Gitはそのような振る舞いをします。 これは、特定のワークフローに従事することを推奨するものではありません。 (私自身の好みは、とにかくコミットすることです。 git stash と、あまり小難しいことは考えず、他の方法を好む人もいます)。

ここでの観察は、あなたが仕事を始めた後に branch1 (別のブランチに切り替えるのが良いことを忘れたり、気づかなかったりする) branch2 を先に実行します。

git checkout branch2

時には、Git が "OK, you're on branch2 now!" と言うこともあれば、Git が "I can't do that, I would lose some of your changes." と言うこともあります。

もしGitが はしません。 をさせると、変更をコミットして永久にどこかに保存する必要があります。 を使用するとよいでしょう。 git stash を保存するために設計されたものです。 なお git stash save または git stash push 実際に というのは すべての変更をコミットし、ブランチには一切手をつけず、現在いる場所から削除します。 そして git stash apply を切り替えることができます。

サイドバーです。 git stash save は古い構文です。 git stash push の引数に関するいくつかの問題を修正するために、Git バージョン 2.13 で導入されました。 git stash と新しいオプションが追加されました。 どちらも基本的な使い方をすれば、同じことができます。

ここで読むのをやめても構いませんよ。

もしGitが しない を使用することで、切り替えることができます。 git stash または git commit または、変更点の再作成が簡単な場合は git checkout -f を使えば強制的にできます。 この回答は、すべて いつ Gitはあなたに git checkout branch2 を変更し始めたにもかかわらず なぜ動作するのか 時々 で、ない。 その他 回ですか?

このルールは、ある意味ではシンプルで、ある意味では複雑で説明しにくいものです。

作業ツリーにあるコミットされていない変更のあるブランチを切り替えてもよいのは、その切り替えがそれらの変更を破棄する必要がない場合のみです。

これはまだ単純化されたものであり、ステージングされたものには非常に難しいコーナーケースがあることに注意してください。 git add s, git rm などにあります。 branch1 . A git checkout branch2 は、こうしなければならないだろう。

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

これらの各ステップは、ワークツリー内の何かを破壊する可能性があります。

  • ファイルを削除することは、ワークツリー内のバージョンが branch1 変更を加えた場合は "unsafe"となります。
  • に表示される方法でファイルを作成する branch2 は、現在存在しない場合は "safe"です。 2 現在存在していても、その内容が "unsafe" であれば、それは "wrong" です。
  • そしてもちろん、あるファイルのワークツリー版を別のバージョンに置き換えることは、そのワークツリー版がすでに branch1 .

新しいブランチの作成 ( git checkout -b newbranch ) は 常に この処理では、ワークツリー内でファイルが追加、削除、変更されることはなく、インデックス/ステージング領域も変更されることはありません。 (注意: 新しいブランチの開始点を変更せずに新しいブランチを作成する場合は安全ですが、別の引数を追加した場合、たとえば git checkout -b newbranch different-start-point に移動するように変更しなければならないかもしれません。 different-start-point . その後、Git は通常通りチェックアウトの安全ルールを適用します)。


1 そのためには、ファイルがブランチに含まれることの意味を定義する必要があります。 ブランチ を適切に設定する必要があります。 (参照 ブランチとはどういう意味ですか? ) ここで、私が本当に言いたいことは branch-nameの解決先となるコミット。 をパスとするファイル P branch1 もし git rev-parse branch1:P はハッシュを生成します。 そのファイル branch1 というエラーメッセージが表示される場合。 パスが存在すること P この質問に答えるのに、インデックスやワークツリーは関係ありません。 したがって、ここでの秘訣は git rev-parse それぞれの branch-name:path . これは、ファイルがせいぜい1つのブランチにしか存在しないため失敗するか、2つのハッシュIDを与えるかのどちらかです。 もし2つのハッシュIDが 同じ の場合、そのファイルは両方のブランチで同じものです。 変更する必要はありません。 ハッシュIDが異なる場合、ファイルは2つのブランチで異なるので、ブランチを切り替えるために変更する必要があります。

ここで重要なのは コミット は永遠に凍結されます。 あなたが編集するファイルは当然 ではなく 凍結されます。 少なくとも最初は、凍結された2つのコミット間のミスマッチだけを見ることになります。 残念ながら、私たち、あるいはGitは、以下のようなファイルも扱わなければなりません。 ではありません。 を使用することができます。 を、切り替え先のコミットで指定します。 なぜなら、ファイルはインデックスやワークツリーにも存在することができ、今回扱う特定のふたつの凍結コミットが存在する必要はないからです。

2 もし、既に正しい内容で存在していれば、quot;sort-of-safe"と見なされ、Gitは結局それを作成する必要がありません。 Gitのいくつかのバージョンでは、これを許可していたように記憶しています。しかし、たった今テストしたところ、Git 1.8.5.4では、これは"unsafe"と見なされることがわかりました。 同じ議論は、変更されたファイルがたまたま to-be-switch-to ブランチにマッチするように変更された場合にも当てはまります。 ここでも、1.8.5.4では単に"would be overwritten"とだけ書かれていますね。 テクニカルノートの最後もご覧ください。私が初めてGitを使い始めたバージョン1.5.sからリードツリーのルールは変わっていないと思うので、私の記憶が間違っているのかもしれません。


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

はい、いくつかの点ではそうです。 特に、変更をステージングしてから、ワークツリー・ファイルを "de-modify"することができます。 次の図は、2つのブランチに分かれているファイルです。 branch1branch2 :

$ 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 のものと一致します。 branch2 にいるにもかかわらず branch1 . この変更はコミット用にステージングされておらず、そのため git status --short がここに表示されています。

$ git status --short
 M inboth

スペースからMは、quot;modified but not staged"(正確には、ワーキングツリーのコピーとステージング/インデックスのコピーが異なる)を意味します。

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

OK、では作業ツリーのコピーをステージングしてみましょう。 branch2 .

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

ここでは、ステージングコピーとワーキングコピーの両方が branch2 ということで、チェックアウトが許可されました。

もう1ステップやってみましょう。

$ 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 ここでの意味は、ステージングされたファイルと HEAD ファイルを作成します。 作業ツリーファイルはステージドファイルと異なります。 ワーキングツリーバージョンは branch1 (別名 HEAD ) バージョンです。

$ 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 が水面下で行っているのは 先端コミット branch2 を、現在のコミットとインデックスの両方にあるものに変更します。 今あるものと一致するファイルは、Git はそのままにしておくことができます。 すべて手つかずです。 の両方で同じであるファイルは コミット は、Git は また これらはブランチの切り替えを可能にするものです。

コミット切り替えを含むGitの多くは、比較的高速です。 なぜなら このインデックスが 実際にインデックスに入っているのは、各ファイルそのものではなく、各ファイルの ハッシュ . ファイルのコピー自体は、Git が呼ぶところの ブロブオブジェクト というように、リポジトリにあります。 これは、コミットにもファイルが格納されているのと同様です。コミットには、実際には ファイル それは、各ファイルのハッシュIDをGitに導くだけなのです。 そのため、GitはハッシュID(現在160ビット長の文字列)を比較して、コミット内容が X Y があります。 同じ ファイルかどうかを判断します。 そして、それらのハッシュIDとインデックス内のハッシュIDを比較することもできます。

これが、上記のような奇妙なコーナーケースにつながるのです。 私たちはコミット X Y どちらもファイル path/to/name.txt に対するインデックス・エントリがあります。 path/to/name.txt . 3つのハッシュはすべて一致するかもしれません。 もしかしたら、2つが一致し、1つが一致しないかもしれません。 もしかしたら、3つとも違うかもしれません。 また、次のような場合もあります。 another/file.txt にしかないものを X または Y で、今インデックスにあるかないか。 これらの様々なケースは、それぞれ個別に検討する必要があります。 が必要です。 をコミットからインデックスにコピーしたり、インデックスから削除したりすることで、コミットからインデックスに切り替えることができます。 X から Y ? もしそうなら、それはまた が必要です。 をワークツリーにコピーするか、ワークツリーから削除します。 そして、もし その そうでない場合、Gitはいくつかのデータを破棄してしまうことになります。

(に記述されています(これらすべての完全なルールは git checkout のドキュメントではなく その git read-tree のドキュメントで、「"Two Tree Merge"」と題されたセクションの下にあります。 .)