Git中級者に送る便利なコマンド群
Gitを使っていて、ちょくちょく便利だなと思うコマンドに出会うので、メモ残しておきます。実際中級者の方には物足りないかもしれませんが、とりあえず。目次は以下。
- 自分がいじったファイルを一旦退避させたい
- ツリーが今どういう状態になっているか確認したい
- 今まで作業をやったことを振り返って、特定の過去に戻りたい
- リモートブランチをチェックアウトしたい
- コンフリクトがあったファイル一覧を表示したい
- 間違ってremote masterブランチにpushしてしまったので、取り消したい
- マージコミットを消したい
- 過去のまとまったコミットをまとめたい
ここから載せるサンプルは、以下のフローが既に処理された前提で話します。
# 適当にファイル作成、push $ touch sample.txt $ git add sample.txt $ git commit -m "initial commit" $ git push origin master # 適当に修正して、amendして、push -f # 2015/07/04 12:04追記 force pushはプルリク出す前のトピックブランチだったら問題ないけども、masterでやるとダメな気がしてきました。 # 他の人がcloneし直なおさないといけない可能性がありますので、追って検証記事を書きます。ご了承ください。 $ echo "amend" >> sample.txt $ git add sample.txt $ git commit --amend -m "initial commit on master" $ git push origin master -f # トピックブランチで修正 $ git checkout -b add_some_feature $ echo "on topic" >> sample.txt $ git add sample.txt $ git commit -m "on topic" # マージしてpush $ git checkout master $ git merge add_some_feature $ git push origin master
自分がいじったファイルを一旦退避させたい
ここまであなたがいくつか修正をしてきたと過程しましょう。そしたら、バグが見つかり、さらに修正をしなければならなくなったとします。そういうときはどうすればよいでしょう。
では、まずバグ修正を以下のようにしているとします。
$ git checkout -b bugfix_for_something $ echo "bugfix" >> sample.txt $ git status On branch bugfix_for_something Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: sample.txt no changes added to commit (use "git add" and/or "git commit -a")
と思ったら、今度はこっちのバグを先に修正してくれと言われてしまいました。自分の修正は今邪魔だけど、でも消すのも辛い。
そんなあなたにgit stash。やってみましょう。
$ git stash Saved working directory and index state WIP on bugfix_for_something: a38f61c on topic HEAD is now at a38f61c on topic # まっさらに戻った $ git status On branch bugfix_for_something nothing to commit, working directory clean # stash listで退避されたものの一覧が確認できる $ git stash list stash@{0}: WIP on bugfix_for_something: a38f61c on topic # リストから戻す $ git stash pop On branch bugfix_for_something Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: sample.txt no changes added to commit (use "git add" and/or "git commit -a") Dropped refs/stash@{0} (e3a93994b233bd9d2faccec2e71cb9735e769c64) # 復活している $ git status On branch bugfix_for_something Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: sample.txt no changes added to commit (use "git add" and/or "git commit -a")
もし、2回stashをやったら、こうなります。
$ echo "bugfix after stash" >> sample.txt # もう一度退避 $ git stash Saved working directory and index state WIP on bugfix_for_something: a38f61c on topic HEAD is now at a38f61c on topic # もはやどっちかわからなくなる $ git stash list stash@{0}: WIP on bugfix_for_something: a38f61c on topic stash@{1}: WIP on bugfix_for_something: a38f61c on topic # その場合はshowを使ってさらに-pをつける $ git stash show stash@{0} -p diff --git a/sample.txt b/sample.txt index b22d237..c01c730 100644 --- a/sample.txt +++ b/sample.txt @@ -1,2 +1,3 @@ amend on topic +bugfix after stash
0番目のほうがあとにstashした修正になるようですね。
では、stashした修正を戻します。
# applyで取り出す $ git stash apply stash@{0} On branch bugfix_for_something Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: sample.txt # applyでは消せない $ git stash list stash@{0}: WIP on bugfix_for_something: a38f61c on topic stash@{1}: WIP on bugfix_for_something: a38f61c on topic # ので消す $ git stash drop stash@{0} Dropped stash@{0} (1a883163d2b76809db49e7ae34cb6b8506f50c20) # 0番に昇格 $ git stash list stash@{0}: WIP on bugfix_for_something: a38f61c on topic # applyとdropを一緒にやる場合はpop。もちろんコンフリクト。 $ git stash pop stash@{0} error: Your local changes to the following files would be overwritten by merge: sample.txt Please, commit your changes or stash them before you can merge. Aborting
というように、退避をさせることで、ブランチを綺麗に扱うことができます。
一旦、退避させたものもコミット、pushさせておきましょう。
$ git add sample.txt $ git commit -m "after stash" $ git checkout master $ git merge bugfix_for_something $ git push origin master
ツリーが今どういう状態になっているか確認したい
ここまでいくつか修正をしましたが、いくつかコミットを重ねると今ツリーがどうなっているかわからなくなることがあります。そんなときに便利なのが、git show-branchです。
$ git show-branch ! [add_some_feature] on topic ! [bugfix_for_something] after stash * [master] after stash --- +* [bugfix_for_something] after stash ++* [add_some_feature] on topic
区切り線の上にあるのはローカルに存在するブランチ名です。
$ git branch add_some_feature bugfix_for_something * master
このように現在3つブランチがあって、masterにいます。 それをもうちょっと表形式のように表現しているわけです。
区切り線の下は、それぞれのツリーが今どういう状態にあるかを示しています。
++* [add_some_feature] on topic
一番下の行にあるこれは表形式で見て、上から眺めるので、3つのブランチすべてがこのコミットを含んでいることを示しています。
+* [bugfix_for_something] after stash
次のこの行は、一番左の列に印が入っていません。一番左はadd_some_featureブランチを指し示すので、そのブランチにはこのコミットが含まれていないことになります。
今まで作業をやったことを振り返って、特定の過去に戻りたい
よく使うコマンドなのですが、好きな過去に戻れるというコマンドがあります。もちろん、ローカルで行った修正だけですが、ローカルで行った修正は.gitディレクトリに歴史が残るので、それで戻れることになります。
ためしに、一番最初にpushしたあとに戻ってみましょう。
まず、git reflogというコマンドを使い、歴史を振り返ります。
$ git reflog e730598 HEAD@{0}: merge bugfix_for_something: Fast-forward a38f61c HEAD@{1}: checkout: moving from bugfix_for_something to master e730598 HEAD@{2}: commit: after stash a38f61c HEAD@{3}: checkout: moving from master to bugfix_for_something a38f61c HEAD@{4}: merge add_some_feature: Fast-forward a42f826 HEAD@{5}: checkout: moving from add_some_feature to master a38f61c HEAD@{6}: commit: on topic a42f826 HEAD@{7}: checkout: moving from master to add_some_feature a42f826 HEAD@{8}: commit (amend): initial commit on master a9de796 HEAD@{9}: commit (initial): initial commit
一番上が新しく、一番下が古いので、一番最初のコミットは以下になります。
a9de796 HEAD@{9}: commit (initial): initial commit
ここまで戻ってみましょう。reset --hardの引数にハッシュ値を渡すとそこまで戻ることができます。
$ git reset --hard a9de796 HEAD is now at a9de796 initial commit $ git log commit a9de79622b62be6ebc83d8f8f7de294568450ceb Author: masudak <masudak@hogehoge.com> Date: Tue Jun 30 22:16:19 2015 +0900 initial commit
戻りました。もちろん、この状態でgit push origin master -fをしたら、この状態がリモートブランチに反映されますが、普段であれば、誰かが既にcloneなりpullしているかもしれないので、影響与えてしまうため、やめておきましょう。
ひとまず、reflogはこういう過去に戻りたいときにすごく便利なコマンドですね。
リモートブランチをチェックアウトしたい
バックアップとして、一旦リモートブランチにpushしておいたものを、違うPCでcloneして、作業したいことがあるかもしれません。
そういうときはgit branch -rが便利です。 一旦、自分の作業をリモートブランチに退避させおきましょう。
$ git checkout add_some_feature $ git push origin add_some_feature $ git branch -a * add_some_feature bugfix_for_something master remotes/origin/add_some_feature remotes/origin/master
pushしました。
以下のように新しいリモートブランチができていますね。
remotes/origin/add_some_feature
では、違う日に違うPCで作業を再開するとして、そのケースを再現してみます。違うディレクトに移って、作業してみましょう。
$ git clone https://github.com/masudaK/git_sample.git git_sample_beta $ cd git_sample_beta $ git branch * master
ということで、masterしかローカルには存在しなくなりました。 でも、作業を再開させたいので、リモートブランチの情報を確認します。
そういうときに便利なのが、git branch -r。
$ git branch -r origin/HEAD -> origin/master origin/add_some_feature origin/master
このように、リモートブランチが表示されます。 あとは、checkoutするだけですね。
$ git checkout -b add_some_feature origin/add_some_feature Branch add_some_feature set up to track remote branch add_some_feature from origin. Switched to a new branch 'add_some_feature' $ git show-branch * [add_some_feature] on topic ! [master] after stash -- + [master] after stash *+ [add_some_feature] on topic
便利ですね。
コンフリクトがあったファイル一覧を表示したい
違うディレクトで作業していましたが、そこでpushしたくなったので、その前にpullしとこうと思い、してみました。
$ echo "waaaaaaaaaaaa" >> sample.txt $ git add sample.txt $ git commit -m "waaaaaaaaaaaa" # コンフリクトがーーーー $ git pull origin master From https://github.com/masudaK/git_sample * branch master -> FETCH_HEAD Auto-merging sample.txt CONFLICT (content): Merge conflict in sample.txt Automatic merge failed; fix conflicts and then commit the result. $ cat sample.txt amend on topic <<<<<<< HEAD waaaaaaaaaaaa ======= bugfix after stash >>>>>>> e730598eba05d06097c90c6d9656c3cb1d048d68
最悪です。今回は1ファイルだけだから、マシですが、実際には複数ファイル出ることもあるでしょう。そんなときはgit ls-files -u。
$ git ls-files -u 100644 b22d2372ded7809262a2aabb8c554a7986de6852 1 sample.txt 100644 352e7bce24c9ca88b93d19eeef799d1ba4bb0735 2 sample.txt 100644 c01c7308b1fa90ed850dcb5b97bb866a9c06dea2 3 sample.txt
ファイルは一つだけでしたね!修正をして、コミットして、pushします。
$ cat sample.txt amend on topic bugfix after stash waaaaaaaaaaaa $ git add sample.txt $ git commit -m "fix conflict"
間違ってremote masterブランチにpushしてしまったので、取り消したい
2015/07/04 11:45追記
この章で述べていることですが、みなが使っている共有レポジトリで行った場合、他の人がcloneしなおしになる可能性があり、force pushはしないほうがいいかもしれないと思ってきました。
そのため、以下に述べた「この方法は、他の人がまだ間違ってpushしてしまったものをpullしたりcloneしていない場合のみ有効です。」という問題ではない可能性がありますので、どう問題があるのか追って検証記事を書くことにします。勉強不足の人間が書いたんだと、この章は温かい気持ちで見守って頂ければ幸いです。
普通に焦ります。なぜスクリプトで防いでなかったのか。 手遅れなので、直しましょう。
再現させてみましょう。まず、今回checkoutしたトピックブランチで開発をし、remote originにpushしましょう。
$ git branch * add_some_feature master $ git push origin add_some_feature:master
masterブランチにはまだマージしておらず、add_some_featureブランチのみが先に進んでいるため、add_some_feature:masterとしないとダメです。 masterが最新の場合は、
$ git push origin master
だけでいけますね。
とりあえず、リモートブランチに間違ってトピックブランチのコミットをしてしまったので、戻しましょう。
$ git reflog 9dd36a8 HEAD@{0}: commit (merge): fix conflict e631696 HEAD@{1}: commit: waaaaaaaaaaaa a38f61c HEAD@{2}: checkout: moving from master to add_some_feature e730598 HEAD@{3}: clone: from https://github.com/masudaK/git_sample.git
cloneしたときのコミットIDがe730598なので、以下のようにします。
$ git reset --hard e730598
$ git log commit e730598eba05d06097c90c6d9656c3cb1d048d68 Author: masudak <masudak@hogehoge.com> Date: Tue Jun 30 22:55:19 2015 +0900 after stash commit a38f61cfde3e71b98a5962dbe118d87fb21fd404 Author: masudak <masudak@hogehoge.com> Date: Tue Jun 30 22:32:49 2015 +0900 on topic commit a42f82641f25c4b3200610b79134d4924f7aaa58 Author: masudak <masudak@hogehoge.com> Date: Tue Jun 30 22:16:19 2015 +0900 initial commit on master
戻りましたね。
そしたら、リモートブランチに反映させましょう。
$ git push -f origin HEAD:master Total 0 (delta 0), reused 0 (delta 0) To git@github.com:masudaK/git_sample.git + 9dd36a8...e730598 HEAD -> master (forced update)
見事に反映されました。あとはGUIで確認すれば、より安心かもしれません。 ちなみに、この方法は、他の人がまだ間違ってpushしてしまったものをpullしたりcloneしていない場合のみ有効です。 そうしないと、force pushでは歴史を強制的に改変してしまうので、整合性が取れなくなってしまうからです。 そういう場合は、revertを使うのですが、それは又の機会に紹介することにしましょう。
マージコミットを消したい
まず、non-fast fowardな修正をマージを行い、マージコミットをしましょう。具体的には、トピックブランチでコミットをしたあと、masterブランチでもコミットを行うようにします。
現在はブランチの状況は以下。
$ git branch * add_some_feature bugfix_for_something master
まずadd_some_featureでコミットを一つします。
$ touch sample_on_topic.txt $ git add sample_on_topic.txt $ git commit -m "add sample_on_topic on add_some_feature"
次にmasterブランチに移り、1回コミットをします。
$ git checkout master $ touch sample_on_master.txt $ git add sample_on_master.txt $ git commit -m "add sample_on_master on master"
そしたら、トピックブランチのコミットをマージしましょう。
$ git merge add_some_feature
そうすると、以下のようなメッセージが出てくるかと思います。
Merge branch 'add_some_feature' # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
$ git log -1 commit 8b3e9438ad53c0affd657caddd774fd803ad5964 Merge: 90aeff6 3ccf906 Author: masudak <masudak@hogehoge.com> Date: Thu Jul 2 20:43:18 2015 +0900 Merge branch 'add_some_feature'
このようにトピックブランチでコミットが進んでいるにも関わらず、masterブランチでもコミットが進むと、基本的にマージはnon fast forwardとなります。歴史が分岐してしまったので、早送りで追いつけないですからね。同じファイルを修正している場合はコンフリクトが起きてしまいますが、今回は別ファイルをいじりましたから、マージもできます。
そして、このマージコミットのログはpushしてもいいのですが、個人的にはあまりログを汚したくないので、消したくなったりします。ということで、消してみましょう。
$ git rebase -i HEAD~1
これを実行すると、こんな画面が出ます。HEAD~1というのは直前のコミットまでを対象にrebaseするということです。
pick 3ccf906 add sample_on_topic on add_some_feature # Rebase 90aeff6..8b3e943 onto 90aeff6 # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out
rebaseはモノによってはpickの文字列をfixupやsquashにしたりしますが、今回はコミット1つでそのコミットもそのまま使うので、「:wq」で保存して大丈夫です。
$ git log -1 commit f33895cdcd21aabc3ba71e9c7bc6a54a0a5796ed Author: masudak <masudak@hogehoge.com> Date: Thu Jul 2 20:41:32 2015 +0900 add sample_on_topic on add_some_feature
これで直前のマージコミットがなくなりました。他にも方法はあるかもしれませんが、この方法は慣れると便利ですよ。
過去のまとまったコミットをまとめたい
今回は少し応用です。時系列順に以下のようなコミットがあるとします。コミットAが一番古く、コミットDが一番新しいものだとします。
- コミットD (ここで実装終わった)
- コミットC (とりあえず保存の意味も込めてコミットした)
- コミットB (とりあえず保存の意味も込めてコミットした)
- コミットA (とりあえず保存の意味も込めてコミットした)
これをまとめて、コミットDのメッセージにまとめ直したいということがある場合にどうするかというのを説明します。
試しに、コミットを4つしてみましょう。
$ echo "commit A" >> sample.txt $ git add sample.txt $ git commit -m "commit A" $ echo "commit B" >> sample.txt $ git add sample.txt $ git commit -m "commit B" $ echo "commit C" >> sample.txt $ git add sample.txt $ git commit -m "commit C" $ echo "commit D" >> sample.txt $ git add sample.txt $ git commit -m "commit D"
4つコミットをしました。
$ git log -4 commit 526bb2c2dd800efa366ca27e527083b58f6d5261 Author: masudak <masudak@hogehoge.com> Date: Thu Jul 2 20:58:15 2015 +0900 commit D commit 66b166c24fe6f109384553987146b0a2c5b0b088 Author: masudak <masudak@hogehoge.com> Date: Thu Jul 2 20:57:50 2015 +0900 commit C commit 79c0d7fe10076a85684b2fe268f8b2e080e4c32b Author: masudak <masudak@hogehoge.com> Date: Thu Jul 2 20:57:29 2015 +0900 commit B commit 73d9b425e8bc0701013e40a1ff391015efc44f6d Author: masudak <masudak@hogehoge.com> Date: Thu Jul 2 20:56:39 2015 +0900 commit A
こういうときはresetを使います。コミットAのハッシュ「73d9b425e8bc0701013e40a1ff391015efc44f6d」を指定して、そこまで戻りましょう。
$ git reset 73d9b425e8bc0701013e40a1ff391015efc44f6d Unstaged changes after reset: M sample.txt bash-3.2$ git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: sample.txt
このようにしてステージから外されますが、以下のように中身はちゃんとそのままです。
$ cat sample.txt amend on topic bugfix after stash commit B on master commit C on master commit A commit B commit C commit D
resetのときに--softも--hardも付けない場合はmixedとなります。
コミットもしているということは、ワーキングツリーとステージ(インデックス)とHEADが同じ位置を指しています。コミットメッセージを直したいので、addしてステージに載ってしまったコミットを一度元に戻したいので、mixedにしています。
もし、ここで--hardしてしまうと、コミットしたデータが全部消えて初期化されてしまいます。--softの場合は以下のようにステージに載ったままとなります。なので、今回の場合はsoftでもmixedでもどちらでも大丈夫です。
$ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: sample.txt
あとは、amendして、コミットを直しましょう。
$ git log -1 commit 73d9b425e8bc0701013e40a1ff391015efc44f6d Author: masudak <masudak@hogehoge.com> Date: Thu Jul 2 20:56:39 2015 +0900 commit A
となっているので、ここにすべて突っ込んでしまうということです。
$ git commit --amend -m "commit A-D" commit 7fdda973e0fc6ddcec202c37bc424487d3a6d0c4 Author: masudak <masudak@hogehoge.com> Date: Thu Jul 2 20:56:39 2015 +0900 commit A-D
これで一つになりました。
終わりに
ここまで僕が過去ハマっていて、かつ昔はどうしたらいいか分からなかったものを思いつく限り載せたつもりです。実際昔はやりかたが分からず、cloneしなおししたりして、本当に無駄なことをしていました。そんな色々ハマっているなか、後輩にやたらgitに詳しい人がいまして、教えてもらいながら、なんとかここまで理解に至ったという感じです。
最近はどうしようもないという事態になることはだいぶ減りましたが、それでもこういうときどうしたらいいんだろうと悩むことも多く、多くの人が悩んでいると思ったので、記事にした次第です。自分も初心者の域からなかなか出られていないため、理解が間違っていたらご指摘お願いします。また、勉強になるので、こういうときどうしたらいいんですか?とかあれば、遠慮なくメンションなりください!ではでは!
続編はこちら。 続・Git中級者に送る便利なコマンド群
この本はgitを深く理解するために、オススメです!