はじめに

Git's main job is to make sure you never lose a committed change. But, it's also designed to give you total control over your development workflow. This includes letting you define exactly what your project history looks like; however, it also creates the potential to lose commits. Git provides its history-rewriting commands under the disclaimer that using them may result in lost content.

この章ではコミット済みのスナップショットを書き換える必要が生じる理由のうち一般的によく起こるものについて説明し、それを実行する際の事故をいかにして防止するかを示します。

git commit --amend

The git commit --amend command is a convenient way to fix up the most recent commit. It lets you combine staged changes with the previous commit instead of committing it as an entirely new snapshot. It can also be used to simply edit the previous commit message without changing its snapshot.

Git チュートリアル: git commit --amend

But, amending doesn’t just alter the most recent commit—it replaces it entirely. To Git, it will look like a brand new commit, which is visualized with an asterisk (*) in the diagram above. It’s important to keep this in mind when working with public repositories.

使用法

git commit --amend

ステージされた変更を直前のコミットと結合し、その結果生成されるスナップショットで直前のコミットを置き換えるコマンドです。ステージエリアに何もない状態でこのコマンドを実行すると、スナップショットを書き換えることなく直前のコミットメッセージの編集を行うことができます。

ディスカッション

Premature commits happen all the time in the course of your everyday development. It’s easy to forget to stage a file or to format your commit message the wrong way. The --amend flag is a convenient way to fix these little mistakes.

公開済みコミットの修正は厳禁

On the git reset page, we talked about how you should never reset commits that have been shared with other developers. The same goes for amending: never amend commits that have been pushed to a public repository.

修正されたコミットは実際には全く新しいコミットであり、直前のコミットはプロジェクトの履歴から削除されます。このことは公開済みのコミットを取り消す場合と同様の問題を生じます。他の開発者が既に作業のベースとして使用しているコミットを修正した場合、彼らの作業のベースが消失したように見えます。これは開発者に混乱をもたらし、それからの回復は面倒です。

The following example demonstrates a common scenario in Git-based development. We edit a few files that we would like to commit in a single snapshot, but then we forget to add one of the files the first time around. Fixing the error is simply a matter of staging the other file and committing with the --amend flag:

# Edit hello.py and main.py git add hello.py git commit # Realize you forgot to add the changes from main.py git add main.py git commit --amend --no-edit

The editor will be populated with the message from the previous commit and including the --no-edit flag will allow you to make the amendment to your commit without changing its commit message. You can change it if necessary, otherwise just save and close the file as usual. The resulting commit will replace the incomplete one, and it will look like we committed the changes to hello.py and main.py in a single snapshot.

git rebase

リベースは、ブランチの基点となるコミットを別のコミットに移動する操作です。一般的な動作を次の図に示します:

Git チュートリアル: プロジェクト履歴の直線性を維持するリベース

見かけ上は、リベースはあるコミットから他のコミットにブランチを移動する手段に過ぎません。しかし Git の内部では、新たなコミットを生成してそれを移動先のベースコミットに適用することによってこれを行なっており、これは即ち文字通りにプロジェクト履歴の書き換えをしていることになります。ここでは、ブランチそのものは同じものに見えていても、それを構成するコミットは全く異なることを理解することが重要です。

使用法

git rebase <base>

Rebase the current branch onto <base>, which can be any kind of commit reference (an ID, a branch name, a tag, or a relative reference to HEAD).

ディスカッション

リベースの主要な目的はプロジェクト履歴の直線性を維持することにあります。例えば、あるフィーチャーでの作業開始後に master ブランチに進行があった状況を考えます:

Git が master でブランチをリベースします

You have two options for integrating your feature into the master branch: merging directly or rebasing and then merging. The former option results in a 3-way merge and a merge commit, while the latter results in a fast-forward merge and a perfectly linear history. The following diagram demonstrates how rebasing onto master facilitates a fast-forward merge.

Git チュートリアル: 早送りマージ

Rebasing is a common way to integrate upstream changes into your local repository. Pulling in upstream changes with git merge results in a superfluous merge commit every time you want to see how the project has progressed. On the other hand, rebasing is like saying, “I want to base my changes on what everybody has already done.”

公開リポジトリのリベースは厳禁

As we’ve discussed with git commit --amend and git reset, you should never rebase commits that have been pushed to a public repository. The rebase would replace the old commits with new ones, and it would look like that part of your project history abruptly vanished.

次の例は、プロジェクトの直線性を維持するために git rebase と git merge を併用したものです。これは、素早く確実に早送りマージを行う手軽な方法です。

# Start a new feature git checkout -b new-feature master # Edit files git commit -a -m "Start developing a feature"

フィーチャー開発中にコードベースにセキュリティホールが発見されたとします。

# Create a hotfix branch based off of master git checkout -b hotfix master # Edit files git commit -a -m "Fix security hole" # Merge back into master git checkout master git merge hotfix git branch -d hotfix

hotfix を master にマージすると、プロジェクト履歴には分岐が発生します。そこで単にgit merge コマンドを使うのではなく、リベースによって履歴の直線性を維持しつつフィーチューの統合を行います:

git checkout new-feature git rebase master

これにより、new-feature は master の先端に移動したので、master との通常の早送りマージが可能となります:

git checkout master git merge new-feature

git rebase -i

Running git rebase with the -i flag begins an interactive rebasing session. Instead of blindly moving all of the commits to the new base, interactive rebasing gives you the opportunity to alter individual commits in the process. This lets you clean up history by removing, splitting, and altering an existing series of commits. It’s like git commit --amend on steroids.

使用法

git rebase -i <base>

Rebase the current branch onto <base>, but use an interactive rebasing session. This opens an editor where you can enter commands (described below) for each commit to be rebased. These commands determine how individual commits will be transferred to the new base. You can also reorder the commit listing to change the order of the commits themselves.

ディスカッション

インタラクティブなリベースを使用することにより、履歴の見かけに対する完全な作り変えが可能になります。この機能によって、コード開発中に乱雑なコミットを繰り返した履歴が残っていたとしても事後にそれを見直して整理することができるため、開発者には大幅な自由が手に入ります。

ほとんどの開発者は、 master ブランチにマージする前にフィーチャーブランチの見栄えをよくするためにインタラクティブなリベースを使用する傾向があります。インタラクティブなリベースを使用すると、重要性の低いコミットを一纏めにし、不要なコミットを削除し、その他すべてを整理してから「公式」なリポジトリにコミットすることができます。事情を知らない者にとっては、このフィーチャー開発が全体的によく計画されたコミットの 1 本の系列として順調に進行したかのように見えます。

The example found below is an interactive adaptation of the one from the non-interactive git rebase page.

# Start a new feature git checkout -b new-feature master # Edit files git commit -a -m "Start developing a feature" # Edit more files git commit -a -m "Fix something from the previous commit" # Add a commit directly to master git checkout master # Edit files git commit -a -m "Fix security hole" # Begin an interactive rebasing session git checkout new-feature git rebase -i master

最後のコマンドによってエディターが開き、ブランチ new-feature で行われた 2 つのコミットを関連情報と共に表示します:

pick 32618c4 Start developing a feature pick 62eed47 Fix something from the previous commit

なお、各々のコミットの前にある pick コマンドは、リベースにおける動作を指定する任意のコマンドに変更することができます。ここでは、squash コマンドを使用して 2 つのコミットを結合するとします:

pick 32618c4 Start developing a feature squash 62eed47 Fix something from the previous commit

Save and close the editor to begin the rebase. This will open another editor asking for the commit message for the combined snapshot. After defining the commit message, the rebase is complete and you should be able to see the squashed commit in your git log output. This entire process can be visualized as follows:

Git Tutorial: git rebase -i example

ここで、結合されたコミットは元のコミットのいずれとも異なる ID を有すること、即ちこのコミットは実は新たなコミットであることに留意してください。

最後に、早送りマージを実行して整理したフィーチャーブランチをメインコードベースに統合します:

git checkout master git merge new-feature

The real power of interactive rebasing can be seen in the history of the resulting master branch—the extra 62eed47 commit is nowhere to be found. To everybody else, it looks like you’re a brilliant developer who implemented the new-feature with the perfect amount of commits the first time around. This is how interactive rebasing can keep a project’s history clean and meaningful.

git reflog

Git では、reflogと呼ばれる機能が働いて、ブランチの先端に対する更新の追跡が行われています。これにより、いかなるブランチからもいかなるタグからも参照されていない更新内容であってもその時点に戻ることができます。履歴を書き換えた後であっても reflog にはブランチの過去の状態が記録されており、必要な場合にはそこに戻ることができます。

使用法

git reflog

ローカルリポジトリの reflog を表示するコマンドです。

git reflog --relative-date

相対形式の日付 (例: 2 週間前) で reflog を表示するコマンドです。

ディスカッション

現在の HEAD において更新 (ブランチの切り替え、新たに加えられた変更のプル、履歴の書き換え、あるいは単なる新規コミットの実行など) が加えられるたびに reflog に新たな項目が追加されます。

To understand git reflog, let's run through an example.

0a2e358 HEAD@{0}: reset: moving to HEAD~2 0254ea7 HEAD@{1}: checkout: moving from 2.2 to master c10f740 HEAD@{2}: checkout: moving from master to 2.2

The reflog above shows a checkout from master to the 2.2 branch and back. From there, there's a hard reset to an older commit. The latest activity is represented at the top labeled HEAD@{0}.

取り消しが意図しないものであった場合でも、2 つのコミットを取り消す前に (0254ea7) をポイントしていたコミットの元情報が reflog に残っています。

git reset --hard 0254ea7

Using git reset it is then possible to change master back to the commit it was before. This provides a safety net in case history was accidentially changed.

なお、変更がローカルリポジトリにコミット済みである場合は reflog が唯一のセーフティネットであること、また reflog は HEAD の移動を記録しているのみであることに留意してください。