カイワレの大冒険 Third

技術的なことや他愛もないことをたまに書いてます

Git中級者に送る便利なコマンド群

Gitを使っていて、ちょくちょく便利だなと思うコマンドに出会うので、メモ残しておきます。実際中級者の方には物足りないかもしれませんが、とりあえず。目次は以下。

ここから載せるサンプルは、以下のフローが既に処理された前提で話します。

# 適当にファイル作成、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を深く理解するために、オススメです!