Git 操作の取り消し・元に戻す手順

Gitクラウド

Git を利用していると、編集内容を元に戻したい、コミットを取り消したい、などの状況がでてくるかと思います。
ここでは Git 操作の取り消し方や戻し方を解説いたします。

参考までに、ソース修正から本番リリースまでの流れを説明した以前の記事をご確認ください。
基本的にこの流れの中で発生しうるもののみを解説していきます。

修正内容のブランチ間の流れ


参考: Git ワークフロー(ブランチモデル)とその手順


ローカルブランチでの編集作業の取り消し(addする前)
ローカルブランチでの作業をすべてなかったことにしたい、あるいは特定のファイルだけ元に戻したいという場合はgit checkoutコマンドを利用します。

# すべて元に戻す
>git checkout .
Updated 2 paths from the index

# 指定のファイルだけ元に戻す
>git checkout {filename}

なお、ファイル追加という編集作業は本コマンドでは元に戻りませんので、git cleanコマンドを利用します。

# 再帰的にディレクトリを辿って削除を行う
>git clean -df
Removing subdir/
Removing work.html


git add の取り消し
git addを取り消したい場合はgit resetコマンドを利用します。
取り消されるのはgit addだけですので、編集内容も元に戻したいのであれば本作業後に上記の「ローカルブランチでの編集作業の取り消し」を行います。

# git addしたすべてのファイルを取り消し
>git reset
Unstaged changes after reset:
D       remove.html
M       update.html

# 指定のファイルのみ取り消し
>git reset {filename}


ローカルブランチでのコミットの取り消し
ワークフロー:手順 ②
コミットしたけどすぐに修正の誤りに気付いたときなど、コミットを取り消したい場合もgit resetコマンドでできます。
オプションによって編集の内容を残したり、git addも取り消したりできます。

# 直前のコミットを取り消し(git addも編集内容も残る)
>git reset --soft HEAD^

# 直前のコミットを取り消し(git addは消去、編集内容は残る)
>git reset --mixed HEAD^
Unstaged changes after reset:
D       remove.html
M       update.html

# 直前のコミットを取り消し(git addも編集内容も消去)
>git reset --hard HEAD^
HEAD is now at 0f35ffd  First Commit

# 指定のコミットまで戻す(指定コミットまでが残る)
>git reset --soft/mixed/hard {復元するコミットID}

git resetを間違ってしまった、取り消したい!、という場合もgit resetです。
復元したい状態のコミット ID を指定してリセットします。
直前のgit resetであれば ORIG_HEAD が使えます。

# 直前のgit resetを取り消す
>git reset --hard ORIG_HEAD

コミット ID の検索は、素のgit logコマンドよりもgit reflogコマンドや--graphオプションをつけたgit logコマンドの方が見やすいかと思います。

>git reflog
54d6f49 (HEAD -> develop, issue01) HEAD@{0}: reset: moving to 54d6f49 # ←ここをなかったことにしたい
02d1534 HEAD@{1}: merge issue01: Merge made by the 'recursive' strategy.
0f35ffd (origin/master, origin/develop, master) HEAD@{2}: checkout: moving from issue01 to develop
54d6f49 (HEAD -> develop, issue01) HEAD@{3}: commit: issue01をコミット
0f35ffd (origin/master, origin/develop, master) HEAD@{4}: checkout: moving from develop to issue01
0f35ffd (origin/master, origin/develop, master) HEAD@{5}: checkout: moving from master to develop
0f35ffd (origin/master, origin/develop, master) HEAD@{6}: commit (initial): First Commit

なお、すでにリモートブランチにマージされたコミットは、このような方法でリセットしてはいけません
git resetコマンドはコミットそのものがなかったことになってしまうため、ログに残らなくなってしまうのです。
当該コミットに影響が及ぶ開発者は、コミットが消えたことになかなか気づけずに苦労することでしょう。
このような場合には後述のgit revertコマンドを利用した方が良いかと思います。
リモートにあがったコミットのログは第三者の目に触れた「周知の事実」となりますので勝手に変えないよう注意しましょう。

Git の柔軟さがデメリットとなるケースです。
Git はその仕組み自体は大変すばらしいのですが、柔軟性の高さゆえにどうあるべきかが見えづらくなっているのが難点ですね。


ローカルブランチ間のマージの取り消し
ワークフロー:手順 ④
ローカルのブランチ同士のマージであれば、これもgit resetで取り消します。
マージの取り消しなので、オプションは--hardが良いでしょう。

# 直前のマージの取り消し
>git reset --hard HEAD^
HEAD is now at 0f35ffd First Commit

# 指定のコミットまで戻す(指定コミットまでが残る)
>git reset --hard {復元するコミットID}


マージの際にコンフリクトが発生した場合の取り消し
ワークフロー:手順 ④
ローカルのdevelopブランチへのマージの際にはコンフリクトが発生する場合があります。
このような場合は--abortオプションですぐに取り消せます。

# コンフリクトの発生したマージの取り消し
>git merge --abort
HEAD is now at 0f35ffd First Commit


リモートブランチへの push の取り消し
ワークフロー:手順 ⑤
すでに言いましたが、リモートブランチにマージされたコミットはリセットしてはいけません
ここでは他の開発者にも何を行ったかが明確になるように、git revertコマンドを利用します。
git revertコマンドは、指定されたコミットを打ち消すコミットを実行します。
これをpushすることで前回のpushが取り消された状態になります。
もちろんブランチを手作業で元に戻してコミットし、pushを行っても構いません。

# マージコミットの打ち消し
>git revert -m 1 {マージコミットのコミットID}
Removing add/add.html
[develop c244c29] Revert "Merge branch 'issue01' into develop"
 3 files changed, 1 insertion(+), 2 deletions(-)
 delete mode 100644 add/add.html
 create mode 100644 remove.html

# リモートブランチへpush
>git push origin develop
Enumerating objects: 6, done.

なおマージの際には保守性の観点から--no-ffオプションをつけることをお勧めします。
コミットツリーで見たときにマージの箇所が見やすくなります。

# issue01ブランチをdevelopブランチにマージする
# --no-ff: fast fowardを抑制しマージコミットを作る
>git checkout develop
>git merge --no-ff issue01

>git log --graph --all --format='%h -%d %s'
* 02d1534 - (HEAD -> develop) Merge branch 'issue01' into develop # ★マージコミット
|\
| * 54d6f49 - (issue01) issue01をコミット
|/
* 0f35ffd - (origin/master, origin/develop, master) First Commit

--no-ffオプションを指定してマージコミットを作成したため、このコミットに向けてgit revertすることでマージ作業を取り消すことができました。

# マージコミットの打ち消し
>git revert -m 1 02d1534

# c244c29(▲)が打ち消しのコミット(★がマージコミット)
>git reflog
c244c29 (HEAD -> develop, origin/develop) HEAD@{0}: revert: Revert "Merge branch 'issue01' into develop" ▲
02d1534 (origin/develop) HEAD@{1}: merge issue01: Merge made by the 'recursive' strategy. ★
0f35ffd (origin/master, master) HEAD@{2}: checkout: moving from issue01 to develop
54d6f49 (issue01) HEAD@{3}: commit: issue01をコミット

>git log --graph --all --format='%h -%d %s'
* c244c29 - (HEAD -> develop, origin/develop) Revert "Merge branch 'issue01' into develop" ▲
* 02d1534 - (origin/develop) Merge branch 'issue01' into develop ★
|\
| * 54d6f49 - (issue01) issue01をコミット
|/
* 0f35ffd - (origin/master, master) First Commit

マージコミットをgit revertする場合は、-mオプションでどちらのブランチの状態に戻すのか指定する必要があります。
今回は取り消し作業なので1を指定します。

>git revert 02d1534
error: commit 02d1534421f9ae4b80168e537f6b2f9cf8446eba is a merge but no -m option was given.
fatal: revert failed

# マージ前の状態にする場合
>git revert -m 1 02d1534

# マージ後の状態にする場合
>git revert -m 2 02d1534


一時的な編集内容の退避
Git ではブランチを移動してもコミットしないかぎりは編集内容(git addも含む)が持ち越されてしまいます。
ブランチを複数掛け持ちながら作業したい場合、特に緊急の障害修正があった場合など、今の編集内容をどこかに退避させたい場面があるかと思います。

>git checkout -b issue02

~ 編集作業 ~

>git checkout issue01
D       remove.html
M       update.html
Switched to branch 'issue01'

>git status
On branch issue01
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    remove.html
        modified:   update.html

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        add/

no changes added to commit (use "git add" and/or "git commit -a")

このような場合にはgit stashコマンドを利用します。
-uオプションを指定することにより、新規のファイル追加も退避することができます。

>git stash save "コメント内容" -u
Saved working directory and index state On issue02: コメント内容

これでブランチが編集前の状態に戻り、他のブランチへ移動しても大丈夫になりました。

>git status
On branch issue02
nothing to commit, working tree clean

>git checkout develop
Switched to branch 'develop'
Your branch is up to date with 'origin/develop'.

git stashで退避したブランチはlistサブコマンドで一覧を見ることができます。

>git stash list
stash@{0}: On issue02: コメント内容
stash@{1}: On issue02: 退避2
stash@{2}: On issue01: 退避1

リスト番号を指定して退避を戻します。
git stash applyは退避したブランチをリストに残したままにします。
git stash popは退避したブランチをリストから削除します。

>git stash apply 0
Removing remove.html
On branch issue01
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    remove.html
        modified:   update.html

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        add/

no changes added to commit (use "git add" and/or "git commit -a")

退避リストの削除にはdropclearサブコマンドがあります。

# リスト番号を指定して特定の退避をリストから削除
>git stash drop 1
Dropped refs/stash@{1} (51173bc08f522d26adaea5681e3de03204c9368f)

>git stash list
stash@{0}: On issue02: コメント内容
stash@{2}: On issue01: 退避1

# リストの全削除
>git stash clear


参考: Git を利用した開発環境・テスト環境・本番環境の構成
参考: Git コンフリクトの解消
参考: Git のフックを利用したデプロイの方法
参考: git push を Chatwork や Slack へ通知する方法

コメント