- はじめに
- add を取り消したい
- 特定のコミットまで戻したい
- 間違えてコードを reset してしまった
- 間違えてマージしてしまった
- 間違えてブランチを消してしまった
- 先に進んでいる別ブランチを反映させたい
- リモートブランチが先に進んでいる場合
- コンフリクトが起きた時
- 別作業をしたいがまだコミットしたくない変更ファイルがある
- さいごに
ここでは、個人開発やチーム開発などでよく起こる「こんな時どうする」ということの解決方法とコマンドの説明をしていきます。
とにかく解決したい!と言う場合にはチートシートを用意しているので、こちらをご覧ください。
まだ編集が完了していないのに、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
コマンドですが、このコマンドには大きく以下の二つの機能を持ちます。
- インデックスを任意の状態に変更
- 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 コマンドを使うとコミットをなかったことに出来ます。
つまり、コミット自体を削除してしまいます。
ですのでコミットログは見やすくなると言うメリットがあります。
しかしこのやり方には注意点があります。
リモートリポジトリに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
を指定すると、ワークツリーもインデックスも元に戻ります。
つまり、指定したコミットをした時の状態に戻るといった感じです。
このオプションはコミットの内容がいらなくなった時などに使用します。
コミットを戻したい時にもう 1 つのやり方としてrevert
コマンドがあります。
このコマンドは、コミットと逆の内容のコミットを作成するコマンドです。
revert
はコミットを取り消すのではなく、逆操作のコミットを追加するだけなのでリモートへ push 済みのコミットをrevert
しても問題はありません。
revert
のログが残るのでログが少し汚くなるというデメリットもありますが、
revert
の方が安全に元に戻したり、revert
のrevert
もできるので個人的にはこちらの方がオススメです。
使い方は以下の通りです。
$ git revert [取り消したいコミットのID]
このコマンドを実行するとエディタが開かれるので、保存すれば OK です。
Pull Request を作成した後脳死でそのままマージをしてしまった...
この Pull Request のマージを取り消したい
というミスをしてしまったことがある人もいると思います。
しかし、GitHiub 上からマージ済みの Pull Request を Revert することができます。
以下のようにクローズされた Pull Request の「Revert」ボタンを選択すると、
Pull Request を revert した Pull Request を作成することが可能です。
これをマージすることによって、コードを元に戻すことができます。
改めて変更点の Pull Request を出したい場合には、Revert した Pull Request をさらに Revert すれば、
本来出したかった Pull Request と同じ内容の Pull Request を作成することが可能です。
しかし、マージした時に発火する Actions などは動いてしまいますし、ログも汚くなるので 1 番は間違えてマージしてしまわないことですね。
--hard
で reset をしてしまい、コードが消えてしまった。
など間違えてリセットしてしまった場合にも解決方法はあります。
先ほどreset
コマンドはコミット自体を削除するので、ログが残らないと言いましたが、
reflog
コマンドを使えば作業ログを出力することができます。
例として、
README.md
の追加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
--hard
でreset
したため、ワークツリーにはすでに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
コマンドを使って作業ログを確認することによって解決することができます。
例として以下の操作を行ったとします。
main
ブランチでREADME
の追加test-1
ブランチを作成&移動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
ブランチはなくなりましたが、以下のようにreflog
でtest-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 ブランチにいるときはHEAD
とmain
は同じ存在であると言えます。
作業番号である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
を使用するという使い分けです。
一見この操作を二度手間のように感じるかもしれませんが、こうすることによってより安全かつログが綺麗になるように開発を進めることができます。
その理由について説明する前に、まずはmerge
とrebase
それぞれの使い方や特徴について説明していきます。
merge
とは元のブランチと派生させたブランチを統合させ、統合したというコミットを作成します。
例として以下の操作を行います。
- README の作成 (main)
test-1
ブランチを切るtest
ファイルの作成 (test-1)test
ファイルの編集 (test-1)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
コマンドは実際には、コミット履歴の移動や削除を行うコマンドですが、
別ブランチからコミットを移動させることによって、ブランチの統合を行うことができます。
ここで、例として先ほどと同じ手順で操作を行い、ログが以下のようになっているとします。
// コミットログ
* 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
これでmain
がtest-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
コマンドはリモートの最新情報をローカルに持ってくるコマンドです。
ただし、このコマンドではローカルのコードは修正されません。単純に情報を取得するだけとなります。
例えば以下の操作を行ったとします。
README
の追加 (main)index.js
の作成 (main)main
ブランチをpush
test-1
ブランチの作成と移動index.js
の編集 (test-1)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/main
や origin/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
main
を origin/main
へ追いつかせたい場合には merge
をするだけです。
$ git merge origin/main
---------------------------------------------------------
Updating 1e85c53..a9a1298
Fast-forward
index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
これで、main
が origin/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
コマンドは fetch
と merge
を同時に行うコマンドなんです。
ただし、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
merge
やrebase
を行う際には、一度 fetch
や pull
を行なってリモートの情報と取得してから実行しましょう。
merge
やrebase
を行った時に、ある程度別々の場所を変更していれば Git が自動でコードを統合してくれますが、
2 つのブランチに同じ箇所の変更があった場合、Git どちらのコードを優先していいか分からずに自動でコードを統合できなくなります。
これをコンフリクト(競合)と言います。
コンフリクトが起きた時は、手動で解消しなければいけません。
ここではコンフリクトを解消する 3 パターンのやり方を説明します。
- merge コマンドでコンフリクトが起きた時
- Pull Request を作成したときにコンフリクトが起きた時
- rebase コマンドでコンフリクトが起きた時
今回は以下の手順で作業したとします。
main
ブランチでREADME
の追加test-1
ブランチに切り替えtest-1
ブランチでindex.js
の作成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
するのはやめましょう。
今回は以下の様に編集をしたとします。
README
の追加(main)test-1
ブランチの作成README
の編集(test-1)index.js
の追加(test-1)README
を 3 と違う内容で編集(main)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.md
とindex.js
でコンフリクトが起きます。
では、実際に Pull Request を作ってみましょう。
すると、以下の様にコンフリクトが発生しているので解消する必要があると言われます。
ですので「Resolve conflicts」を選択すると、以下のような画面になります。
こちらも先ほどの様に、
<<<<<<< test-1
// test-1 ブランチの内容
=======
// main ブランチの内容
>>>>>>> main
という意味になります。
コンフリクトを直したら、右上の「Mark as resolved」を選択します。
すると、解決したものにチェックされ、以下の様に別のコンフリクトがあればそのファイルへ行きます。
コンフリクトが起きているすべてのファイルを解消したら以下の画像のように、
「Commit merge」ボタンを選択します。
これで問題なく Pull Request をマージすることができるようになりました。
Pull Request を作成するとコンフリクトを解消していないとマージができないので、安全にマージすることはできますが、
このやり方では、GitHub 上で編集しているので本当に動くかをローカルで試す前にマージしてしまう危険性もあります。
また、ここでやっている操作としては、
main
ブランチをtest-1
ブランチにmerge
- 起きたコンフリクトを解消
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
を用いたコンフリクトの解消が 2 つのやり方よりも安全で 1 番オススメです。
しかし、少しだけ難しいかもしれません。慣れたら大丈夫なのでたくさん練習しましょう。
まず、例として以下の作業を行います。
README
の作成(main)test-1
ブランチの作成index.js
の追加(test-1)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 つずつ移動させるので、コミット毎でコンフリクトが起きます。
ですので、一気にすべて解消する必要がありません。
例として、以下の操作を行なったとします。
README
の追加(main)test-1
ブランチの作成README
の編集(test-1)index.js
の追加(test-1)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-1
でmain
をrebase
すると、
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
している変更をもとに戻すコマンドですが、
apply
はstash 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 に書いてください!