このチュートリアルでは、ソフトウェア開発プロジェクトの過去のバージョンに関する操作を行う際に必要になるすべてのスキルを学習します。最初に、過去のコミットを調べる方法を示し、次に公開リポジトリに公開済みのコミットの打ち消しとローカルマシーン上での未公開のコミットの取り消しの違いを説明します。

Git checkout

The git checkout command serves three distinct functions: checking out files, checking out commits, and checking out branches. In this module, we’re only concerned with the first two configurations.

コミットのチェックアウトを行うと、作業ディレクトリがそのコミットと完全に一致した状態になります。このコマンドは、プロジェクトの現在の状態を一切変更することなく過去の状態を確認する場合に使用します。ファイルのチェックアウトを行うと、作業ディレクトリの他の部分に一切影響を与えることなくそのファイルの過去のリビジョンを確認することができます。

使用法

git checkout master

master ブランチに戻るコマンドです。ブランチについては次の章で詳しく説明しますが、ここではとりあえず master ブランチはプロジェクトの「現在の」状態に戻る手段だと考えてください。

git checkout <commit> <file>

Check out a previous version of a file. This turns the <file> that resides in the working directory into an exact copy of the one from <commit> and adds it to the staging area.

git checkout <commit>

Update all files in the working directory to match the specified commit. You can use either a commit hash or a tag as the <commit> argument. This will put you in a detached HEAD state.

ディスカッション

The whole idea behind any version control system is to store “safe” copies of a project so that you never have to worry about irreparably breaking your code base. Once you’ve built up a project history, git checkout is an easy way to “load” any of these saved snapshots onto your development machine.

過去のコミットのチェックアウトは、読み取り専用の操作となります。過去のバージョンを閲覧することによってリポジトリが何らかの影響を受けることはありません。master ブランチのプロジェクトの「現在の」状態には、一切変更は加えられません (詳細については、ブランチの章をご覧ください)。一般的な開発プロジェクトの進行中、HEAD は通常 master ブランチまたはその他のローカルブランチを指しますが、過去のコミットをチェックアウトすると、HEAD はブランチではなく直接コミットを指すようになります。この状態を "detached HEAD" 状態と呼び、図で説明すると次のようになります。

Git チュートリアル: 過去のコミットのチェックアウト

On the other hand, checking out an old file does affect the current state of your repository. You can re-commit the old version in a new snapshot as you would any other file. So, in effect, this usage of git checkout serves as a way to revert back to an old version of an individual file.

Git トレーニング: ファイルの過去のバージョンのチェックアウト

過去のバージョンの閲覧

この例では、常識外れの開発を実験的に開始したものの、それを保存しておくべきか否かについて判断ができないものと仮定します。そしてこの判断の参考とするため、実験的開発を開始する前のプロジェクトの状態を確認するとします。最初に、確認するバージョンのIDを知る必要があります。

git log --oneline

次のようなプロジェクト履歴が表示されたとします:

b7119f2 Continue doing crazy things
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.py
435b61d Create hello.py
9773e52 Initial import

You can use git checkout to view the “Make some import changes to hello.py” commit as follows:

git checkout a1e8fb5

This makes your working directory match the exact state of the a1e8fb5 commit. You can look at files, compile the project, run tests, and even edit files without worrying about losing the current state of the project. Nothing you do in here will be saved in your repository. To continue developing, you need to get back to the “current” state of your project:

git checkout master

ここでは、デフォルトである master ブランチで開発作業をしているものと仮定しています (master ブランチについては、ブランチ の章で詳しく説明します)。

master ブランチに戻った後、git revert または git reset コマンドを使用して、不要と判断した変更を元に戻すことができます。

ファイルのチェックアウト

確認したいファイルが 1 つだけである場合も、git checkout コマンドを使用してそのファイルの過去のバージョンをフェッチできます。例えば、過去のコミットに含まれる特定のファイル hello.py だけを確認したい場合は、次のコマンドを使用します。

git checkout a1e8fb5 hello.py

コミットのチェックアウトとは異なり、これはプロジェクトの現在の状態に影響を与えます。古いファイルのリビジョンは、"コミットする変更" として表示され、以前のバージョンのファイルに戻すことができます。古いバージョンを保持しない場合は、次のようにして最新のバージョンをチェックアウトすることができます。

git checkout HEAD hello.py

Git revert

git revert はコミットされたスナップショットを元に戻すコマンドです。ただし、プロジェクト履歴においてそのコミットがなかったことにするのではなく、そのコミットによって加えられた変更を元に戻し、その結果を含む新しいコミットを追加します。これは Git の履歴を保全するためであり、バージョン履歴の完全性の維持とコラボレーションの信頼性の確保のために重要です。

Git チュートリアル: git の打消し

使用法

git revert <commit>

Generate a new commit that undoes all of the changes introduced in <commit>, then apply it to the current branch.

ディスカッション

Reverting should be used when you want to remove an entire commit from your project history. This can be useful, for example, if you’re tracking down a bug and find that it was introduced by a single commit. Instead of manually going in, fixing it, and committing a new snapshot, you can use git revert to automatically do all of this for you.

打消しと取消し

git revert は、一つのコミットのみを元に戻すコマンドであることをしっかりと理解してください。そのコミットの後に行われたすべてのコミットを削除することによってプロジェクトを以前の状態に「戻す」コマンドではありません。Git では以前の状態に戻すコマンドは reset であり、revert ではありません。

Git チュートリアル: 打消しと取消し

Reverting has two important advantages over resetting. First, it doesn’t change the project history, which makes it a “safe” operation for commits that have already been published to a shared repository. For details about why altering shared history is dangerous, please see the git reset page.

Second, git revert is able to target an individual commit at an arbitrary point in the history, whereas git reset can only work backwards from the current commit. For example, if you wanted to undo an old commit with git reset, you would have to remove all of the commits that occurred after the target commit, remove it, then re-commit all of the subsequent commits. Needless to say, this is not an elegant undo solution.

git revert コマンドの簡単な使用例を次に示します。ここでは、あるスナップショットをコミットした直後に、その操作を revert で元に戻しています。

# Edit some tracked files
# Commit a snapshot
git commit -m "Make some changes that will be undone"
# Revert the commit we just created
git revert HEAD

これは、次のように視覚化できます。

Git チュートリアル: git revert の例

Note that the 4th commit is still in the project history after the revert. Instead of deleting it, git revert added a new commit to undo its changes. As a result, the 3rd and 5th commits represent the exact same code base, and the 4th commit is still in our history just in case we want to go back to it down the road.

Git のリセット

If git revert is a “safe” way to undo changes, you can think of git reset as the dangerous method. When you undo with git reset(and the commits are no longer referenced by any ref or the reflog), there is no way to retrieve the original copy—it is a permanent undo. Care must be taken when using this tool, as it’s one of the only Git commands that has the potential to lose your work.

Like git checkout, git reset is a versatile command with many configurations. It can be used to remove committed snapshots, although it’s more often used to undo changes in the staging area and the working directory. In either case, it should only be used to undo local changes—you should never reset snapshots that have been shared with other developers.

使用法

git reset <file>

作業ディレクトリに何の変更も加えずに、指定したファイルをステージングエリアから削除するコマンドです。このコマンドを実行すると、変更を書き込むことなく指定したファイルをアンステージします。

Git のリセット

Reset the staging area to match the most recent commit, but leave the working directory unchanged. This unstages all files without overwriting any changes, giving you the opportunity to re-build the staged snapshot from scratch.

git reset --hard

Reset the staging area and the working directory to match the most recent commit. In addition to unstaging changes, the --hard flag tells Git to overwrite all changes in the working directory, too. Put another way: this obliterates all uncommitted changes, so make sure you really want to throw away your local developments before using it.

git reset <commit>

Move the current branch tip backward to <commit>, reset the staging area to match, but leave the working directory alone. All changes made since <commit> will reside in the working directory, which lets you re-commit the project history using cleaner, more atomic snapshots.

git reset --hard <commit>

Move the current branch tip backward to <commit> and reset both the staging area and the working directory to match. This obliterates not only the uncommitted changes, but all commits after <commit>, as well.

ディスカッション

All of the above invocations are used to remove changes from a repository. Without the --hard flag, git reset is a way to clean up a repository by unstaging changes or uncommitting a series of snapshots and re-building them from scratch. The --hard flag comes in handy when an experiment has gone horribly wrong and you need a clean slate to work with.

Whereas reverting is designed to safely undo a public commit, git reset is designed to undo local changes. Because of their distinct goals, the two commands are implemented differently: resetting completely removes a changeset, whereas reverting maintains the original changeset and uses a new commit to apply the undo.

Git チュートリアル: 打消しと取消し

公開済み履歴の取り消しは厳禁

You should never use git reset <commit> when any snapshots after <commit> have been pushed to a public repository. After publishing a commit, you have to assume that other developers are reliant upon it.

他の開発者が開発中に行ったコミットを取り消すとコラボレーション上の深刻な問題が生じます。彼らがあなたのリポジトリとの同期を行おうとすると、プロジェクト履歴のある範囲が欠落したように見えます。公開済みのコミットを取り消すと何が起こるかを以下の一連の図に示します。ここで origin/master ブランチは、ローカルな master ブランチに対応する中央リポジトリのブランチです。

Git チュートリアル: 公開済み履歴の取り消し

As soon as you add new commits after the reset, Git will think that your local history has diverged from origin/master, and the merge commit required to synchronize your repositories is likely to confuse and frustrate your team.

The point is, make sure that you’re using git reset <commit> on a local experiment that went wrong—not on published changes. If you need to fix a public commit, the git revert command was designed specifically for this purpose.

ファイルのアンステージ

The git reset command is frequently encountered while preparing the staged snapshot. The next example assumes you have two files called hello.py and main.py that you’ve already added to the repository.

# Edit both hello.pyand main.py
# Stage everything in the current directory
git add .
# Realize that the changes in hello.pyand main.py
# should be committed in different snapshots
# Unstage main.py
git reset main.py
# Commit only hello.py
git commit -m "Make some changes to hello.py"
# Commit main.py in a separate snapshot
git add main.py
git commit -m "Edit main.py"

As you can see, git reset helps you keep your commits highly-focused by letting you unstage changes that aren’t related to the next commit.

ローカルなコミットの削除

次の例はより高度なユースケースを示します。ここでは実験的開発をしばらく行っていると仮定し、いくつかのスナップショットをコミットした後でそれらをすべて破棄する場合にどうするべきかを示します。

# Create a new file called `foo.py` and add some code to it
# Commit it to the project history
git add foo.py
git commit -m "Start developing a crazy feature"
# Edit `foo.py`again and change some other tracked files, too
# Commit another snapshot
git commit -a -m "Continue my crazy feature"
# Decide to scrap the feature and remove the associated commits
git reset --hard HEAD~2

The git reset HEAD~2 command moves the current branch backward by two commits, effectively removing the two snapshots we just created from the project history. Remember that this kind of reset should only be used on unpublished commits. Never perform the above operation if you’ve already pushed your commits to a shared repository.

git clean

git clean は、作業ディレクトリから追跡対象外のファイルを削除するコマンドです。git status を使用して追跡対象外のファイルを確認し、手作業で削除することもできますが、このコマンドを使用すれば、より効率よく同じ操作を行うことができます。通常の rm コマンドと同様、git clean コマンドも元に戻すことはできないため、このコマンドを実行する際には、その追跡対象外ファイルを本当に削除して良いかどうかをよく確認してください。

git clean コマンドは、git reset --hard コマンドとよく併用されます。既に説明したように reset コマンドが作用するのは追跡対象となっているファイルのみであるため、追跡対象外のファイルをクリーンアップするためには別のコマンドが必要となります。この 2 つのコマンドを併用すると、作業ディレクトリを、ある特定のコミットの時点と完全に同じ状態に戻すことができます。

使用法

git clean -n

git clean の「予行演習」を行うコマンドです。このコマンドを実行すると削除されるファイルを表示しますが、実際の削除は行われません。

git clean -f

Remove untracked files from the current directory. The -f (force) flag is required unless the clean.requireForce configuration option is set to false (it's true by default). This will not remove untracked folders or files specified by .gitignore.

git clean -f <path>

追跡対象外のファイルを削除しますが、その対象範囲は指定したパスに限定するコマンドです。

git clean -df

Remove untracked files and untracked directories from the current directory.

git clean -xf

カレントディレクトリ内の追跡対象外ファイルおよび Git では通常無視されるファイルを削除します。

ディスカッション

git reset --hardgit clean -f の 2 つのコマンドは、ローカルリポジトリでの思わしくない作業の痕跡を消したい場合に便利です。この 2 つのコマンドを実行すると、作業ディレクトリは直前のコミットが行われた時点の状態に戻り、その状態から作業をやり直すことができます。

The git clean command can also be useful for cleaning up the working directory after a build. For example, it can easily remove the .o and .exe binaries generated by a C compiler. This is occasionally a necessary step before packaging a project for release. The -x option is particularly convenient for this purpose.

なお、git clean コマンドが git reset コマンドと同じくコミットの内容を完全に削除してしまう数少ない Git コマンドのひとつであり、使用には注意が必要であることを忘れてはなりません。実際重要なコミットの内容を失ってしまうことが非常に多いため、基本的な操作であるにもかかわらず Git メンテナーが -f フラグの指定を "必須" に設定することがよくあります。これを設定しておくと、単純な git clean の実行によって意図せずにすべてを失うことを防止できます。

次の例は、作成された新規ファイルを含めて作業ディレクトリ内のすべての変更を取り消すものです。ここでは、既にいくつかのスナップショットをコミットしており、また新たな実験的開発の途中にあると仮定しています。

# 既存のファイルの編集
# 新規ファイルの追加
# 自分が何をしているのかが分からないことを認識する

# 追跡対象ファイルの変更を元に戻す
git reset --hard

# 追跡対象外のファイルの削除
git clean -df

この一連の reset/clean コマンドを実行すると、作業ディレクトリおよびステージングエリアは直前のコミット時と全く同一となり、git status コマンドを実行すると、作業ディレクトリの内容がクリーンであることが示されます。この状態から、新たに作業をやり直すことができます。

Note that, unlike the second example in git reset, the new files were _not _added to the repository. As a result, they could not be affected by git reset --hard, and git clean was required to delete them.