Skip to content

Latest commit

 

History

History
1181 lines (859 loc) · 46 KB

07.md

File metadata and controls

1181 lines (859 loc) · 46 KB

こんな時どうする

【目次】

  1. はじめに
  2. add を取り消したい
  3. 特定のコミットまで戻したい
  4. 間違えてコードを reset してしまった
  5. 間違えてマージしてしまった
  6. 間違えてブランチを消してしまった
  7. 先に進んでいる別ブランチを反映させたい
  8. リモートブランチが先に進んでいる場合
  9. コンフリクトが起きた時
  10. 別作業をしたいがまだコミットしたくない変更ファイルがある
  11. さいごに

【はじめに】

ここでは、個人開発やチーム開発などでよく起こる「こんな時どうする」ということの解決方法とコマンドの説明をしていきます。

とにかく解決したい!と言う場合にはチートシートを用意しているので、こちらをご覧ください。

【add を取り消したい】

まだ編集が完了していないのに、add してしまった。
add する予定のないファイルまで add してしまった。

などなど、add を取り消したい時は少なくありません。

安心してください。add を取り消したい時には以下のコマンドを実行するだけで大丈夫です。

// 全てのadd取り消す
$ git reset HEAD
or
$ git reset

// 特定のファイルのみ取り消す
$ git reset HEAD [ファイルのパス]
or
$ git reset [ファイルのパス]

resetコマンドはデフォルトでHEADが指定されるため、HEADは付けても付けなくても OK です。

HEADとは一言で言うならば自分が今作業をしている場所のポインタです。
つまり、現在自分が今いるブランチの最新のコミットを表しています。

log で履歴を見てみると、現在のブランチの最新のコミットに HEAD と示されていることが確認できます。

// mainブランチでlog
$ git log
----------------------------------------------------------------------------
commit 808c1b847fbc245cd17b31122ad12140610a5cf2 (HEAD -> main, origin/main)
Merge: 527161f 87b2c04
Author: Takeru  Miki <take.cantik17@gmail.com>
Date:   Mon Dec 12 11:04:52 2022 +0900

    Merge pull request #1 from take-cantik/test-1

    hello関数の作成

commit 87b2c04297cd7a356d08899f58d1e6070e7ad7b6 (origin/test-1, test-1)
Author: take-cantik <take.cantik17@gmail.com>
Date:   Sun Dec 11 19:13:39 2022 +0900

    Update: delete test and add hello func

commit 527161f482b839ecf8658b6135a1f09f69640eab
Author: take-cantik <take.cantik17@gmail.com>
Date:   Mon Dec 5 01:19:43 2022 +0900

    Update: README
// test-1ブランチでlog
$ git log
----------------------------------------------------------------------------
commit 87b2c04297cd7a356d08899f58d1e6070e7ad7b6 (HEAD -> test-1, origin/test-1)
Author: take-cantik <take.cantik17@gmail.com>
Date:   Sun Dec 11 19:13:39 2022 +0900

    Update: delete test and add hello func

commit 527161f482b839ecf8658b6135a1f09f69640eab
Author: take-cantik <take.cantik17@gmail.com>
Date:   Mon Dec 5 01:19:43 2022 +0900

    Update: README

commit 7b2ea0f4d20df2a0623b2e06f74eaf7e54d1e2ed
Author: take-cantik <take.cantik17@gmail.com>
Date:   Mon Dec 5 01:19:19 2022 +0900

    Add: test func

次にresetコマンドですが、このコマンドには大きく以下の二つの機能を持ちます。

  1. インデックスを任意の状態に変更
  2. HEAD を指定したコミットに移動

今回は 1 番目の機能を使用しています。

つまり、

$ git reset HEAD

を行うと、インデックスの状態を HEAD と同じ状態に戻す。つまり、add したものが削除されると言うことになります。

ここで、注意する点があります。
それはファイル削除の add をファイル指定で HEAD を付けずに reset しようとした時です。

ワーキングツリーにファイルがない場合にはパスが見つからないと怒られてしまいます。

$ git reset index.js
-----------------------------------------------------------------------------------------
fatal: ambiguous argument 'index.js': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
 ~/works/hackz/test/test-app

しかし、第二引数にHEADを入れると問題なく元に戻すことができます。

$ git reset HEAD index.js
----------------------------------------
Unstaged changes after reset:
D index.js

なんとなくで覚えていると詰まる時もあるので、常にHEADをつけることを意識するとなんの問題もありません。

また、git init直後のHEADが存在しないタイミングでの add を取り消したい場合には以下のコマンドを実行します。

// 全てのaddを取り消す
$ git rm --cached -r .

// 特定のファイルのaddを取り消す
$ git rm --cached [ファイルのパス]

rmコマンドは、ワークツリーとインデックスからファイルを削除するコマンドです。

これを行うとファイルも消えてしまうのですが、--cachedを指定することによりファイルをインデックスのみから削除できるようになります。
また、-rを指定するとディレクトリをまとめて削除することが可能になります。

【特定のコミットまで戻したい】

間違えてコミットしてしまった。
実装はしたが、コードが不要になってしまった。

など、ある場所までコミットを戻したい時があると思います。

コミットを戻す時は主に以下の2つの方法があります。

  • resetコマンド
  • revertコマンド

reset コマンドでコミットを戻す

reset コマンドを使うとコミットをなかったことに出来ます。
つまり、コミット自体を削除してしまいます。
ですのでコミットログは見やすくなると言うメリットがあります。

しかしこのやり方には注意点があります。
リモートリポジトリにpushしたコミットに対して行うと、不具合が発生してしまいます。 コミットそのものを削除してしまうので、既に取り消したコミットの上にさらに誰かがコミットしている場合に、
存在するはずの親コミットがなくなってしまい、push することができなくなってしまいます。

したがって、resetコマンドを使用する場合には、まだリモートに push していないローカルのみのコミット、または自分しか手をつけていないブランチのコミットのみに使用するようにしてください。

使い方は以下の通りです。

// ワークツリーとインデックスはそのままにしたい時
$ git reset --soft [戻りたいコミットのID]

// ワーキングツリーはそのままでコミットを戻したい時
$ git reset --mixed [戻りたいコミットのID]

// 全てを戻したい時
$ git reset --hard [戻りたいコミットのID]

コミットの ID はlogコマンドで出力される英数字の羅列となります。

reset のオプションについては以下の通りです。

--softを指定すると、コミットのみが取り消されます。
つまり、それまでのファイルの変更や add した内容はそのままになります。
このオプションはまだコミットする予定がないのにコミットしてしまった時や、コミットするファイルを間違えてしまった時などに使うと便利なものです。

--mixedを指定すると、コミットとインデックスの中身も削除します、
ちなみにオプション指定なしだとデフォルトでこのオプションがつきます。
add 取り消しの時に $ git reset HEAD をしたと思いますが、このコマンドで add が取り消されるのは最後のコミットまでインデックスも戻すからということになります。

--hardを指定すると、ワークツリーもインデックスも元に戻ります。
つまり、指定したコミットをした時の状態に戻るといった感じです。
このオプションはコミットの内容がいらなくなった時などに使用します。

revert コマンドでコミットを戻す

コミットを戻したい時にもう 1 つのやり方としてrevertコマンドがあります。

このコマンドは、コミットと逆の内容のコミットを作成するコマンドです。
revertはコミットを取り消すのではなく、逆操作のコミットを追加するだけなのでリモートへ push 済みのコミットをrevertしても問題はありません。

revertのログが残るのでログが少し汚くなるというデメリットもありますが、
revertの方が安全に元に戻したり、revertrevertもできるので個人的にはこちらの方がオススメです。

使い方は以下の通りです。

$ git revert [取り消したいコミットのID]

このコマンドを実行するとエディタが開かれるので、保存すれば OK です。

【間違えてマージしてしまった】

Pull Request を作成した後脳死でそのままマージをしてしまった...
この Pull Request のマージを取り消したい
というミスをしてしまったことがある人もいると思います。

しかし、GitHiub 上からマージ済みの Pull Request を Revert することができます。

以下のようにクローズされた Pull Request の「Revert」ボタンを選択すると、
Pull Request を revert した Pull Request を作成することが可能です。

スクリーンショット 2022-12-21 17 24 15

スクリーンショット 2022-12-21 17 24 28

これをマージすることによって、コードを元に戻すことができます。

改めて変更点の Pull Request を出したい場合には、Revert した Pull Request をさらに Revert すれば、
本来出したかった Pull Request と同じ内容の Pull Request を作成することが可能です。

しかし、マージした時に発火する Actions などは動いてしまいますし、ログも汚くなるので 1 番は間違えてマージしてしまわないことですね。

【間違えてコードを reset してしまった】

--hardで reset をしてしまい、コードが消えてしまった。 など間違えてリセットしてしまった場合にも解決方法はあります。

先ほどresetコマンドはコミット自体を削除するので、ログが残らないと言いましたが、
reflogコマンドを使えば作業ログを出力することができます。

例として、

  1. README.mdの追加
  2. index.jsの追加

をやった時に、reflogをしてみます。

$ git reflog
-----------------------------------------------------
bde95ea (HEAD -> main) HEAD@{0}: commit: Add: index
404d1c0 HEAD@{1}: commit (initial): Add: README

このように自分のやった作業ログを確認できます。
では、実際に間違ってresetしてしまったと言う体で最初のコミットまで戻ってreflogをしてみます。

すると出力は以下のようになります。

$ git reflog
---------------------------------------------------------------
404d1c0 (HEAD -> main) HEAD@{0}: reset: moving to 404d1c0
bde95ea HEAD@{1}: commit: Add: index
404d1c0 (HEAD -> main) HEAD@{2}: commit (initial): Add: README

--hardresetしたため、ワークツリーにはすでにindex.jsは存在していませんし、
$ git logを出してもindex.jsを追加したコミットは出力されません。

しかしreflogを行うと、このようにresetを行ったという作業ログが出力されます。
HEAD@{n}が作業番号ということになります。

ですので、index.jsを追加した場所つまり、HEAD@{1}に戻ればいいので、

$ git reset --hard HEAD@{1}

をしてあげると、なんとresetしたindex.jsを追加したコミットを復活させることができます。

実際に作業ログを改めて確認してみると、以下のように出力されます。

$ git reflog
------------------------------------------------------------
bde95ea (HEAD -> main) HEAD@{0}: reset: moving to HEAD@{1}
404d1c0 HEAD@{1}: reset: moving to 404d1c0
bde95ea (HEAD -> main) HEAD@{2}: commit: Add: index
404d1c0 HEAD@{3}: commit (initial): Add: README

ここで分かるように、HEADの位置が最初にresetをしたbde95eaになっていることが確認できます。

このように--hardで消してしまっても作業ログを辿れば問題なく復元させることができるので、焦らないようにしましょう。

【間違えてブランチを消してしまった】

作業中のブランチをまだ push していなかったのに消してしまった。

という時があるかもしれません。
そんな時も、reflogコマンドを使って作業ログを確認することによって解決することができます。

例として以下の操作を行ったとします。

  1. mainブランチでREADMEの追加
  2. test-1ブランチを作成&移動
  3. test-1ブランチでindex.jsの追加

この操作を行ってreflogをしてみると、

$ git reflog
--------------------------------------------------------------
58c824f (HEAD -> test-1) HEAD@{0}: commit: Add: index
4377528 (main) HEAD@{1}: checkout: moving from main to test-1
4377528 (main) HEAD@{2}: commit (initial): Add: README

このように出力されます。
ここでtest-1を消してみます。

これでtest-1ブランチはなくなりましたが、以下のようにreflogtest-1ブランチでの操作記録が残っています。

$ git reflog
--------------------------------------------------------------
4377528 (HEAD -> main) HEAD@{0}: checkout: moving from test-1 to main
58c824f HEAD@{1}: commit: Add: index
4377528 (HEAD -> main) HEAD@{2}: checkout: moving from main to test-1
4377528 (HEAD -> main) HEAD@{3}: commit (initial): Add: README

ここで、branchコマンドを使ってHEAD@{1}test-1ブランチへコピーしてみます。

$ git branch test-1 HEAD@{1}

ここで、ブランチを確認してみると

$ git branch
* main
  test-1

と、このようにtest-1ブランチが復活していることがわかります。

branchコマンドはブランチを作成することができますが、第 2 引数にブランチを指定すると
そのブランチから新しいブランチを生やすことができます。

// developブランチからtest-1ブランチを生やす
$ git branch test-1 develop

ここで、ブランチとは実はコミットを表すポインタにすぎません。
つまり main ブランチにいるときはHEADmainは同じ存在であると言えます。

作業番号であるHEAD@{n}も同様にその場所を示すポインタにすぎないので、

$ git branch test-1 HEAD@{1}

を行うと、HEAD@{1}の示す状態からtest-1ブランチを生やしたということになり、
test-1が復活したように見える、というわけです。

【先に進んでいる別ブランチを反映させたい】

チーム開発をしているときに、自分のブランチより先に進んでいるブランチを自分のブランチに反映させたい時があります。

そんな時は。mergeコマンドまたは、rebaseコマンドを使用することによって解決することができます。
これらのコマンドはどちらもブランチ同士を統合するためのコマンドですが、統合のやり方が異なります。

よくある使い方の例としては、
main ブランチへ Pull Request を出す前にブランチの派生元が進んでいる場合には、自分のブランチへmainブランチをrebaseして、
そのブランチをmainブランチへmergeさせるために Pull Request を作成、その後mergeする。

といった使用方法になります。

つまり、

  • 先に進んでいるブランチが反映させたいブランチの派生元であれば rebase
  • 先に進んでいるブランチが反映させたいブランチの派生先であれば merge

を使用するという使い分けです。

一見この操作を二度手間のように感じるかもしれませんが、こうすることによってより安全かつログが綺麗になるように開発を進めることができます。 その理由について説明する前に、まずはmergerebaseそれぞれの使い方や特徴について説明していきます。

merge

mergeとは元のブランチと派生させたブランチを統合させ、統合したというコミットを作成します。

例として以下の操作を行います。

  1. README の作成 (main)
  2. test-1ブランチを切る
  3. testファイルの作成 (test-1)
  4. testファイルの編集 (test-1)
  5. indexファイルの追加 (main)
// コミットログ
* f1e3838 take-cantik  (HEAD -> main) Add: index file
| * bea436f take-cantik  (test-1) Add: console log to test
| * 7645861 take-cantik  Add: test file
|/
* d9eba18 take-cantik  Add: README

ここで、mainブランチへtest-1ブランチをマージしてみます。

// mainブランチにて
$ git merge test-1

mergeした後にログを確認してみると、

// コミットログ
*   e7fec15 take-cantik  (HEAD -> main) Merge branch 'test-1'
|\
| * bea436f take-cantik  (test-1) Add: console log to test
| * 7645861 take-cantik  Add: test file
* | f1e3838 take-cantik  Add: index file
|/
* d9eba18 take-cantik  Add: README

このように、ブランチが統合されマージをしたというコミット(一番上のログ)が作成されます。

しかし、基本的にはmergeはコマンドで行わず、Pull Request を作成してマージした方が安全ですので、
特にチーム開発ではmergeコマンドは使わないようにしましょう。

rebase

rebaseコマンドは実際には、コミット履歴の移動や削除を行うコマンドですが、
別ブランチからコミットを移動させることによって、ブランチの統合を行うことができます。

ここで、例として先ほどと同じ手順で操作を行い、ログが以下のようになっているとします。

// コミットログ
* f1e3838 take-cantik  (HEAD -> main) Add: index file
| * bea436f take-cantik  (test-1) Add: console log to test
| * 7645861 take-cantik  Add: test file
|/
* d9eba18 take-cantik  Add: README

ログの通り、test-1を分岐させたタイミングのmainより現在のmainの方が進んでいる状態なので、
mainの内容をtest-1に反映させます。

// test-1ブランチにて
$ git rebase main

これで、mainの情報がtest-1へ反映されました。
この時のログは以下のようになります。

// コミットログ
* 83c0a55 take-cantik  (HEAD -> test-1) Add: console log to test
* 01a6be7 take-cantik  Add: test file
* f1e3838 take-cantik  (main) Add: index file
* d9eba18 take-cantik  Add: README

このように進んでいたmainのコミットの上にtest-1で行ったコミットが乗っているのがわかります。

ここで、注意して欲しいのがtest-1で行ったコミットの ID が変わっているということです。

rebaseを使用するとtest-1で行ったコミットがmainの上にコミットが移動します。
コミットはこれまでのコミット履歴に紐付いているので、ID も変わるという仕組みになります。
同じ内容のコミットを新たにmainに加えると考えるといいでしょう。

このため、みんなが触るようなブランチでrebaseを行うとコミット履歴が変わってしまうためreset同様おかしくなってしまいます。
従って、基本的には Pull Request 作成前にコミットログを綺麗にする時や、自分でしか触らないブランチなどで使用するといいでしょう。

またもう一点、コミットの ID が変更されているためrebase後のpushには-fを指定して、強制プッシュをしなければいけないことに注意してください。

先ほど、よくある使い方の例として、

main ブランチへ Pull Request を出す前にブランチの派生元が進んでいる場合には、自分のブランチへmainブランチをrebaseして、
そのブランチをmainブランチへmergeさせるために Pull Request を作成、その後mergeする。

一見この操作を二度手間のように感じるかもしれませんが、こうすることによってより安全かつログが綺麗になるように開発を進めることができます。

といいましたが、実際にmainブランチの履歴をrebaseで持ってきたtest-1ブランチをmainブランチへmergeしてみます。

// mainブランチにて
$ git rebase test-1

これを行ってログを見てみると

// rebaseを行ってmergeしたときのログ
* 83c0a55 take-cantik  (HEAD -> main, test-1) Add: console log to test
* 01a6be7 take-cantik  Add: test file
* f1e3838 take-cantik  Add: index file
* d9eba18 take-cantik  Add: README

これでmaintest-1に追いつきましたね。
このように、ただmergeした時よりログが綺麗になっていることがわかります。

// mergeのみを行った時のログ
*   e7fec15 take-cantik  (HEAD -> main) Merge branch 'test-1'
|\
| * bea436f take-cantik  (test-1) Add: console log to test
| * 7645861 take-cantik  Add: test file
* | f1e3838 take-cantik  Add: index file
|/
* d9eba18 take-cantik  Add: README

【リモートブランチが先に進んでいる場合】

自分以外の人がコードを push したり、Pull Request を作成して GitHub 上でマージしたりなど
ローカルよりリモートの方が進んでいて、いちいちすべて pull するのは、、と思う人もいるでしょう。

そういうときには、fetchコマンドを使用します。
fetchコマンドはリモートの最新情報をローカルに持ってくるコマンドです。

ただし、このコマンドではローカルのコードは修正されません。単純に情報を取得するだけとなります。

例えば以下の操作を行ったとします。

  1. READMEの追加 (main)
  2. index.jsの作成 (main)
  3. mainブランチをpush
  4. test-1ブランチの作成と移動
  5. index.jsの編集 (test-1)
  6. test-1ブランチをpush
// ログ
* fb7f467 take-cantik  (HEAD -> test-1, origin/test-1) Update: change word to test-1
* 1e85c53 take-cantik  (origin/main, main) Add: index js
* 34dbc1a take-cantik  first commit

この様に、リモートのブランチは origin/mainorigin/test-1の様に表示され、 現状はコードを編集して push しただけなので、リモートもローカルも同じ位置に存在します。

では、この test-1ブランチを mainへ GitHub 上からマージします。 しかし、マージしたことをローカルは知らないので、ここで fetch を使って情報を取ってきます。

$ git fetch

すると、ログが以下の様になります。
この様に origin/main は進んでいて、ローカルの main はそのままの状態となっています。

*   a9a1298 GitHub  (origin/main) Merge pull request #1 from take-cantik/test-1
|\
| * fb7f467 take-cantik  (HEAD -> test-1, origin/test-1) Update: change word to test-1
|/
* 1e85c53 take-cantik  (main) Add: index js
* 34dbc1a take-cantik  first commit

mainorigin/main へ追いつかせたい場合には merge をするだけです。

$ git merge origin/main
---------------------------------------------------------
Updating 1e85c53..a9a1298
Fast-forward
 index.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

これで、mainorigin/main に追いつくことができました!

*   a9a1298 GitHub  (HEAD -> main, origin/main) Merge pull request #1 from take-cantik/test-1
|\
| * fb7f467 take-cantik  (origin/test-1, test-1) Update: change word to test-1
|/
* 1e85c53 take-cantik  Add: index js
* 34dbc1a take-cantik  first commit

この操作は pull コマンドと同じ動きをしていることにお気づきでしょうか。
この様に、実は pull コマンドは fetchmerge を同時に行うコマンドなんです。

ただし、pullでは指定したブランチ以外の情報は取って来ないので、いろんなブランチのリモートの情報が欲しいときには fetch を使用して取得しましょう。

また、-p を指定した場合にはリモートで削除されたブランチを削除するので、
リモートでtest-1ブランチを削除し、-pを指定してfetchを行いログを見てみると、以下の様に origin/test-1 が消えていることが確認できます。

*   a9a1298 GitHub  (HEAD -> main, origin/main) Merge pull request #1 from take-cantik/test-1
|\
| * fb7f467 take-cantik  (test-1) Update: change word to test-1
|/
* 1e85c53 take-cantik  Add: index js
* 34dbc1a take-cantik  first commit

mergerebaseを行う際には、一度 fetchpull を行なってリモートの情報と取得してから実行しましょう。

【コンフリクトが起きた時】

mergerebaseを行った時に、ある程度別々の場所を変更していれば Git が自動でコードを統合してくれますが、
2 つのブランチに同じ箇所の変更があった場合、Git どちらのコードを優先していいか分からずに自動でコードを統合できなくなります。

これをコンフリクト(競合)と言います。

コンフリクトが起きた時は、手動で解消しなければいけません。
ここではコンフリクトを解消する 3 パターンのやり方を説明します。

  1. merge コマンドでコンフリクトが起きた時
  2. Pull Request を作成したときにコンフリクトが起きた時
  3. rebase コマンドでコンフリクトが起きた時

merge コマンドでコンフリクトが起きた時

今回は以下の手順で作業したとします。

  1. mainブランチでREADMEの追加
  2. test-1ブランチに切り替え
  3. test-1ブランチでindex.jsの作成
  4. mainでもindex.jsの作成(3 とは違う内容)

この様に、index.jsを両ブランチ同士で編集した状態でmergeしようとするとコンフリクトが起きます。
では、実際にやってみます。

$ git merge test-1
-------------------------------------------------------------------
Auto-merging index.js
CONFLICT (add/add): Merge conflict in index.js
Automatic merge failed; fix conflicts and then commit the result.

この様にコンフリクトが起きると、どのファイルでコンフリクトが起きているか確認することができます。
CONFLICT (add/add): Merge conflict in index.js
実際にindex.jsを見てみると、

<<<<<<< HEAD
console.log("index")
=======
console.log("test-1");
>>>>>>> test-1

この様に、

<<<<<<< HEAD
// mainブランチの内容
=======
// test-1ブランチの内容
>>>>>>> test-1

のように表示されます。
ですので、この内容を手動で書き換えます。
今回は以下の様にmainブランチを優先したいと思います。

console.log("index");

この様にファイルを変更し終えたら、変更したファイルをadd, commitします。
すると、mergeが完了します。

// ログ
*   f29a275 take-cantik  (HEAD -> main) Merge branch 'test-1'
|\
| * 98d2075 take-cantik  (test-1) Add: console test
* | 2517191 take-cantik  Add: console index
|/
* ceb5714 take-cantik  Add: README

ちなみに、複数ファイルでコンフリクトが起きていた場合には、以下の様に出力されます。

$ git merge test-1
---------------------------------------------------------------------
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Auto-merging index.js
CONFLICT (add/add): Merge conflict in index.js
Automatic merge failed; fix conflicts and then commit the result.

この様な場合には、すべてコンフリクトを解決してコミットをしてください。

しかし、この操作はmainブランチを直接編集しているので、安全ではありません。
解決はできますが、mainブランチに直接mergeするのはやめましょう。

Pull Request でコンフリクトが起きた時

今回は以下の様に編集をしたとします。

  1. READMEの追加(main)
  2. test-1ブランチの作成
  3. READMEの編集(test-1)
  4. index.jsの追加(test-1)
  5. READMEを 3 と違う内容で編集(main)
  6. index.jsを 4 と違う内容で編集(main)
// ログ
* 5621bad take-cantik  (HEAD -> main) Add: index
* 9ed3b5b take-cantik  Update: README for main
| * c9b670e take-cantik  (test-1) Add: test
| * 74e10cc take-cantik  Update: README
|/
* caa8557 take-cantik  Add: README

この操作を行うと、README.mdindex.jsでコンフリクトが起きます。
では、実際に Pull Request を作ってみましょう。

すると、以下の様にコンフリクトが発生しているので解消する必要があると言われます。

スクリーンショット 2022-12-23 10 54 25

ですので「Resolve conflicts」を選択すると、以下のような画面になります。

スクリーンショット 2022-12-23 10 54 49

こちらも先ほどの様に、

<<<<<<< test-1
// test-1 ブランチの内容
=======
// main ブランチの内容
>>>>>>> main

という意味になります。
コンフリクトを直したら、右上の「Mark as resolved」を選択します。

すると、解決したものにチェックされ、以下の様に別のコンフリクトがあればそのファイルへ行きます。

スクリーンショット 2022-12-23 10 55 17

コンフリクトが起きているすべてのファイルを解消したら以下の画像のように、
「Commit merge」ボタンを選択します。

スクリーンショット 2022-12-23 10 55 36

これで問題なく Pull Request をマージすることができるようになりました。

スクリーンショット 2022-12-23 10 55 54

Pull Request を作成するとコンフリクトを解消していないとマージができないので、安全にマージすることはできますが、
このやり方では、GitHub 上で編集しているので本当に動くかをローカルで試す前にマージしてしまう危険性もあります。

また、ここでやっている操作としては、

  1. mainブランチをtest-1ブランチにmerge
  2. 起きたコンフリクトを解消
  3. mainの内容を含んだtest-1ブランチをmainブランチへmerge

と 2 回mergeされていることがわかります。
ですので、ログが以下の様にとても汚くなってしまいます。

*   f8f6955 GitHub  (HEAD -> main) Merge pull request #1 from take-cantik/test-1
|\
| *   10dc36e GitHub  Merge branch 'main' into test-1
| |\
| |/
|/|
* | 5621bad take-cantik  (origin/main) Add: index
* | 9ed3b5b take-cantik  Update: README for main
| * c9b670e take-cantik  (origin/test-1, test-1) Add: test
| * 74e10cc take-cantik  Update: README
|/
* caa8557 take-cantik  Add: README

個人的にはこちらのやり方もオススメできません。

rebase でコンフリクトが起きた時

ここで紹介するrebaseを用いたコンフリクトの解消が 2 つのやり方よりも安全で 1 番オススメです。
しかし、少しだけ難しいかもしれません。慣れたら大丈夫なのでたくさん練習しましょう。

まず、例として以下の作業を行います。

  1. READMEの作成(main)
  2. test-1ブランチの作成
  3. index.jsの追加(test-1)
  4. index.jsを 3 と違う内容で追加(main)
// ログ
* 24de48d take-cantik  (HEAD -> main) Add: main index
| * c1f3566 take-cantik  (test-1) Add: test-1 index
|/
* cf39ba1 take-cantik  Add: README

これでindex.jsでコンフリクトが発生します。

では、実際にtest-1ブランチでmainブランチをrebaseしてみます。

$ git rebase main
-----------------------------------------------------------------------------------------
Auto-merging index.js
CONFLICT (add/add): Merge conflict in index.js
error: could not apply c1f3566... Add: test-1 index
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply c1f3566... Add: test-1 index

するとこの様に、c1f3566のコミットでコンフリクトが起きていることを示されます。

このタイミングで自分のブランチを確認してみると、

$ git branch
---------------------------------------
* (no branch, rebasing test-1)
  main
  test-1

と、rebase中のブランチのないところにいる状態になります。
このタイミングではブランチの移動などができない状態になりますので、注意しましょう。

では、実際にコンフリクトが起きているファイルを見てみます。

<<<<<<< HEAD
console.log("main")
=======
console.log("test-1")
>>>>>>> c1f3566 (Add: test-1 index)

するとこの様に表示されるため、またこの内容を編集して行きます。
編集が終わったらadd, commitします。

$ git add index.js
$ git com -m "Fix: resolved conflict"
------------------------------------------------------------
[detached HEAD 0794225] Fix: resolved conflict
 1 file changed, 1 insertion(+), 1 deletion(-)

コミットが終わって、コンフリクトを解消したら以下のコマンドでrebaseを終了します。

$ git rebase --continue
------------------------------------------------------------
Successfully rebased and updated refs/heads/test-1.

これでrebaseが完了しました。

// ログ
* 0794225 take-cantik  (HEAD -> test-1) Fix: resolved conflict
* 24de48d take-cantik  (main) Add: main index
* cf39ba1 take-cantik  Add: README

ここでも、test-1ブランチをリモートに反映させる場合は-fを指定して強制プッシュする必要があります。

先ほど、rebaseコマンドはコミットをrebase元の最新に移動すると言いましたが、
rebaseでコンフリクトが起きると、この移動させたコミットを上書きするというイメージになります。

ですので、commitの際に-mを指定しなくても、元々のコミットメッセージが入っているので、
そのままコミットするとログが以下の様になります。

コミットメッセージを書き換えてしまうと、何をやったコミットなのかが分からなくなるので、なるべくrebase時は-mを指定せずにコミットした方がいいと思います。
以下が書き換えなかった場合のログになります。

* aceac68 take-cantik  (HEAD -> test-1) Add: test-1 index
* 24de48d take-cantik  (main) Add: main index
* cf39ba1 take-cantik  Add: README

また、rebaseのメリットとしてはコミットを 1 つずつ移動させるので、コミット毎でコンフリクトが起きます。 ですので、一気にすべて解消する必要がありません。

例として、以下の操作を行なったとします。

  1. READMEの追加(main)
  2. test-1ブランチの作成
  3. READMEの編集(test-1)
  4. index.jsの追加(test-1)
  5. READMEの編集とindex.jsの追加(main)
* f08cc60 take-cantik  (HEAD -> main) Add: README ans indexjs
| * dd40e75 take-cantik  (test-1) Add: index js
| * 2e3ba32 take-cantik  Update: README for test
|/
* 46a80f6 take-cantik  Add: README

この時、test-1mainrebaseすると、
3 番の操作(2e3ba32 のコミット)が main の上に移動、4 番の操作(dd40e75 のコミット)がその更に上に移動という順番で処理が行われます。

ですので、それぞれでコンフリクトが起きます。
実際にやってみます。

// mainをrebase
$ git rebase main
--------------------------------------------------------------------------------------------
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
error: could not apply 2e3ba32... Update: README for test
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 2e3ba32... Update: README for test

この様に、まずは2e3ba32のコミットでコンフリクトが起きます。
これを解決してadd, commitが終わったらcontinueをします。

// README.md編集後
$ git add README.md
$ git commit
$ git rebase --continue
--------------------------------------------------------------------------------------------
Auto-merging index.js
CONFLICT (add/add): Merge conflict in index.js
error: could not apply dd40e75... Add: index js
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply dd40e75... Add: index js

すると、この様に次はdd40e75のコミットでコンフリクトが起きます。
先ほどと同様に、これを解決してadd, commit, continueを行います。

// index.js編集後
$ git add index.js
$ git commit
$ git rebase --continue
------------------------------------------------------
Successfully rebased and updated refs/heads/test-1.

これでrebaseが完了します。
ログは以下のとおりです。

* c4aedf9 take-cantik  (HEAD -> test-1) Add: index js
* 0b3df20 take-cantik  Update: README for test
* f08cc60 take-cantik  (main) Add: README ans indexjs
* 46a80f6 take-cantik  Add: README

これで、test-1ブランチからmainブランチへコンフリクトがない状態で安全にマージすることができます。

【別作業をしたいがまだコミットしたくない変更ファイルがある】

ファイルを変更している途中に、ローカルの情報を反映させたいときがあると思います。
しかし、pullしようとするとワークツリーに変更点があるためできません。
また、今すぐにやりたい作業ができ、別ブランチに行こうにもワークツリーに変更点があれば邪魔になります。

こんな時に仕方なく途中のコミットをする、とりあえず変更点を消す、などをする必要はありません。

stashコマンドを用いれば、変更点を一旦隠して保持することができます。

$ git stash save

or

$ git stash

ここで注意することが、ファイルを新規作成している場合はまだそのファイルが Git の追跡対象になっていないので、コードを退避することができません。
そのような場合には、-uを指定してstashコマンドを使用します。

$ git stash save -u

or

$ git stash -u

また、.envなどの ignore しているファイルの変更も-aを指定すると退避してくれます。

$ git stash save -a

or

$ git stash -a

また、コミットメッセージのように stash で隠す際にもメッセージを残すことができます。
stash したリストを確認するときに何をしていたのか分かりやすくなるので、すぐに戻す場合ではない限りメッセージ付きでstashすることをオススメします。

メッセージをつける場合には、saveを付けないといけません。

$ git stash save "[メッセージ]"
$ git stash save -u "[メッセージ]"
$ git stash save -a "[メッセージ]"

stashしたものは、以下のコマンドで一覧を見ることができます。

$ git stash list

出力は以下の様に最新の stash から順に表示されます。
コメントをつけるとコメントが、つけない場合にはどの時の HEAD のコミット ID が出力されます。
stash@{n}はそのstashの番号だと思ってください。

stash@{0}: On main: test2
stash@{1}: On main: test1
stash@{2}: WIP on main: c4aedf9 Add: index js

それぞれのstashは詳細を見ることが可能です。
以下のコマンドで詳細を見ることができます。

// 最新のstash(ここではstash@{0})の詳細を見る
$ git stash show

// 番号を指定して詳細を見る
$ git stash show stash@{2}

or

$ git stash show 2

すると以下の様に、変更ファイル、それぞれの行の変更数、変更ファイル数、などが確認できます。

$ git stash show
--------------------------------------------------
 index.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

また、-pを指定することによって退避分の差分も確認することができます。
diffコマンドのように出力されます。

$ git stash show stash@{2} -p
--------------------------------------------------
diff --git a/index.js b/index.js
index 65eff05..adc5b0e 100644
--- a/index.js
+++ b/index.js
@@ -1,2 +1,2 @@
-// Comment
+// Comment test
 console.log("test")

では、stashした変更をもとに戻したい時があります。
その様な時はstash applyまたは、stash popコマンドを使用します。

どちらもstashしている変更をもとに戻すコマンドですが、
applystash listで確認できる一覧から削除されず、
popはリストから削除されます。

ですので、そのブランチのみでしか使わない様な変更だとpop、複数ブランチで反映させたい場合などにapplyを使うと良いでしょう。
以下は使い方の例です。

// 直近のstashを反映(listから削除)
$ git stash pop

// 直近のstashを反映(listに残す)
$ git stash apply

// stash番号を指定して変更を反映(listから削除)
$ git stash pop stash@{1}

or

$ git stash pop 1

// stash番号を指定して変更を反映(listに残す)
$ git stash apply stash@{1}

or

$ git stash apply 1

これで退避したものをもとに戻すことができます。

ここで注意したいのが、退避した時からそのファイルを変更した場合にはコンフリクトが発生します。
そのときに、コミットをしていない状態だとapplyまたはpopを行ってもエラーになります。

ですので、その場合には一旦コミットを行なってから反映させてください。
コンフリクトが起きた場合には、ファイルが以下の様に変更されます。

// Comment
<<<<<<< Updated upstream
console.log("test")
console.log("test")

=======
console.log("test2")
>>>>>>> Stashed changes

次にもういらないstashを消して行きます。
stash dropまたは、stash clearコマンドで消すことが出来ます。

// 直近のstashを削除
$ git stash drop

// 番号を指定して削除
$ git stash drop stash@{1}

or

$ git stash drop 1

// すべてのstashを削除
$ git stash clear

このようにstashコマンドを使用することによって安全にコードを対比させることができます。

【さいごに】

これ以外にもこんな場合の解決方法が欲しいという場合には、Issue に書いてください!

前の資料 次の資料