チタン アーマー:さまざまな障害からの復旧

Nicola Paolucci
Nicola Paolucci
リストに戻る

git は高度なツールです。git の哲学は、私が大切にしている考え方でもあります。開発者を賢明で責任感のある人として扱うことです。つまり、git は多くのパワーを人に授けます。同時に、このパワーは災いを招くもとになります。皆さんはおそらくチタン アーマーで防御しているでしょうが、それでも自らを傷つけることがあります。

The topic of this post is to confirm what my colleague Charles O'Farrell said very well in the ever popular "Why Git?" article some time ago:

[...] Git is actually the safest of all the DVCS options. As we saw above, Git never actually lets you change anything, it just creates new objects.

[...] Git actually keeps track of every change you make, storing them in the reflog. Because every commit is unique and immutable, all the reflog has to do is store a reference to them. This means you're safe from harm and your code is always preserved, but there are situations where you might need some conjuring to get it back.

実際のトラブルから回復する方法を、単純なものから重度のものまで、いくつか実例を挙げてみましょう。

Table Of Contents

  1. How To Undo A (Soft) Reset And Recover A Deleted File
  2. If You Lose A Commit During An Interactive Rebase
  3. How To Undo reset --hard If You Only Staged Your Changes

How To Undo A (Soft) Reset And Recover A Deleted File

If you delete a file with git rm and immediately after you git reset your working directory (which is called a soft reset), you will find yourself with a missing file and a dirty working directory like the following:

On branch master
Changes not staged for commit:
(use "git add/rm file..." to update what will be committed)
(use "git checkout -- file..." to discard changes in working directory)
deleted: test.txt

There are a couple of simple ways to go about restoring the file. One is to use a git reset --hard which will recreate the missing files but it will delete any local modifications you might have in your working directory.

The second is to just re-check out the file yourself with git checkout test.txt.

In a slightly different scenario, if the file was removed in an earlier commit, you can recover it by noting down the exact commit where the file was deleted and use the reference to pick the commit immediately before: git checkout commit_id~ -- test.txt

Where the ~ (tilde) sign means the one before this one.

If You Lose A Commit During An Interactive Rebase

Interactive rebase is one of the most useful tools in the git arsenal; It allows to edit, squash and delete commits interactively.

Personally, I love to clean and streamline commits before sharing them with others. It's nice when each commit is an understandable and logical chunk of work. In the heat of development, or in a local private branch, I might commit furiously for a bit, then when I am satisfied I spend time cleaning up the history.

But what if while in the interactive rebase session you accidentally remove a commit line without realizing it and save?

This situation is slightly annoying cause you have just removed a commit from your history!

If you created a throwaway branch before starting the rebase - which you should always do - you're fine and you can restart the process all over. But what if you didn't?

No worries, git has got you covered. Nothing is ever lost. This is a good opportunity to use the [reflog][6, which is an automatic mechanism recording where the tip of all branches has been for the past 30 days.

Say at the start of the rebase I had 4 commits like the following:

$ git log
cfdf880 [2 seconds ago] (HEAD, master) wrote fifth change
ab446e6 [34 seconds ago] changed one line
6e1a130 [25 minutes ago] third change
6566977 [26 minutes ago] second change
5feeb33 [26 minutes ago] initial commit

I rebase the last 4 commits and accidentally remove one line:

$ git rebase -i HEAD~4

エディターが開き、以下のように表示されます。

pick 6566977 second change
pick 6e1a130 third change
pick ab446e6 changed one line
pick cfdf880 wrote fifth change
Rebase 5feeb33..cfdf880 onto 5feeb33

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

I removed one commit line (pick ab446e6 changed one line), I saved and got this:

Successfully rebased and updated refs/heads/master.

Now, if I check the list of commits I see I lost one:

$ git log
3dd6845 [6 minutes ago] (HEAD, master) wrote fifth change
6e1a130 [32 minutes ago] third change
6566977 [32 minutes ago] second change
5feeb33 [32 minutes ago] initial commit

But the reflog has our lost commit:

$ git reflog
3dd6845 HEAD@{0}: rebase -i (finish): returning to refs/heads/master
3dd6845 HEAD@{1}: rebase -i (pick): wrote fifth change
6e1a130 HEAD@{2}: checkout: moving from master to 6e1a130
cfdf880 HEAD@{3}: commit: wrote fifth change
----}} ab446e6 HEAD@{4}: commit: changed one line
[...]

Here ab446e6 is the sha-1 for the commit we lost. Armed with this id we can just cherry-pick it back in the code:

$ git cherry-pick ab446e6
[master 032c6a6] changed one line
1 file changed, 1 insertion(+), 1 deletion(-)

ご覧のように、コミットは履歴に戻っています。

$ git log
032c6a6 [12 minutes ago] (HEAD, master) changed one line
3dd6845 [12 minutes ago] wrote fifth change
6e1a130 [37 minutes ago] third change
6566977 [38 minutes ago] second change
5feeb33 [38 minutes ago] initial commit

How To Undo reset --hard If You Only Staged Your Changes

短期間だけ使用する非公開のブランチであれ、共有のブランチであれ、皆さんが頻繁にコミットして、問題の山から身を守っていることは言うまでもないでしょう。

But let's say you didn't commit this time and you find yourself in the following scenario: you've done some work, haven't committed it yet but have git added it to the staging area.

Then tragedy strikes: you type git reset --hard and immediately realize that you've zeroed your local changes(!!) and brought back (in it's entirety) the previous commit.

This is a tough situation to be in. How do you recover your work? Is it even possible? The answer is yes! The solution involves some low level searching and can be approached in a couple of ways.

Since git creates new objects in the .git/objects folder as soon as something is added to the staging area we can look there for the most recently created objects:

In OSX (and BSD systems) you can do:

find .git/objects/ -type f | xargs stat -f "%Sm %N" | sort | tail -5

Linux(GNU ツール使用)では、次のコマンドが使えます。

find .git/objects/ -type f -printf '%TY-%Tm-%Td %TT %p\n' | sort | tail -5

With tail -n you can cut to the last n results if your repository is very big:

The result should be something like:

Apr 2 12:26:33 2013 .git/objects//pack/pack-4cf657357973915f6fb6f90a41f69a88c0b08bb7.pack
Apr 2 12:29:56 2013 .git/objects//3d/d6845a69cd59dac0851e1ae0bd69dde950c46b
Apr 2 12:29:56 2013 .git/objects//68/c983f0cefb4bee2f753516ab6352ee2f2b7d29
Apr 2 12:35:23 2013 .git/objects//03/2c6a653cc00c302020293e020d8d68326b112e
Apr 2 15:34:55 2013 .git/objects//df/485610889e98ff773b4440a032ea2ede1338b9

In my case my sample file was lost exactly around 15:34 so I can inspect and retrieve it with:

git show df485610889e98ff773b4440a032ea2ede1338b9

Remember that the folder is part of the sha-1 reference, so remove the / slash to compute the correct id.

Note that git has specific low level command to explore dangling objects and to verify their connectivity: git fsck. For an alternative solution to this problem using fsck, check out this very cool answer on Stack Overflow.

結論

このテーマで取り扱うべき実例やシナリオは数多くあるので、今度また、もっと興味深いケースをお話しする機会があると思います。

One thing to take away from this should be the feeling that it's very very hard to screw up and lose your data when using git. That's all for now!

Follow the awesome @Bitbucket team or me @durdn for more Git rocking.

Git を学習する準備はできていますか?

この対話式チュートリアルを利用しましょう。

今すぐ始める