-
1. Gitを始めるにあたって
- 1.1 バージョン管理について
- 1.2 Gitの歴史
- 1.3 Gitとは何か?
- 1.4 コマンドライン
- 1.5 Gitのインストール
- 1.6 Gitの初回セットアップ
- 1.7 ヘルプの利用
- 1.8 まとめ
-
2. Gitの基本
- 2.1 Gitリポジトリの取得
- 2.2 リポジトリへの変更の記録
- 2.3 コミット履歴の表示
- 2.4 元に戻す操作
- 2.5 リモートでの作業
- 2.6 タグ付け
- 2.7 Gitエイリアス
- 2.8 まとめ
-
3. Gitのブランチ機能
- 3.1 ブランチの基本
- 3.2 基本的なブランチとマージ
- 3.3 ブランチ管理
- 3.4 ブランチワークフロー
- 3.5 リモートブランチ
- 3.6 リベース
- 3.7 まとめ
-
4. サーバー上のGit
- 4.1 プロトコル
- 4.2 サーバーにGitをセットアップする
- 4.3 SSH公開鍵の生成
- 4.4 サーバーのセットアップ
- 4.5 Gitデーモン
- 4.6 スマートHTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 サードパーティのホスティングオプション
- 4.10 まとめ
-
5. 分散Git
- 5.1 分散ワークフロー
- 5.2 プロジェクトへの貢献
- 5.3 プロジェクトの管理
- 5.4 まとめ
-
6. GitHub
- 6.1 アカウントのセットアップと設定
- 6.2 プロジェクトへの貢献
- 6.3 プロジェクトの管理
- 6.4 組織の管理
- 6.5 GitHubのスクリプト
- 6.6 まとめ
-
7. Gitツール
- 7.1 リビジョンの選択
- 7.2 インタラクティブステージング
- 7.3 スタッシュとクリーン
- 7.4 作業に署名する
- 7.5 検索
- 7.6 履歴の書き換え
- 7.7 Reset の解明
- 7.8 高度なマージ
- 7.9 Rerere
- 7.10 Gitを使ったデバッグ
- 7.11 サブモジュール
- 7.12 バンドル
- 7.13 置換
- 7.14 認証情報の保存
- 7.15 まとめ
-
8. Gitのカスタマイズ
- 8.1 Gitの設定
- 8.2 Git属性
- 8.3 Gitフック
- 8.4 Gitによるポリシー適用例
- 8.5 まとめ
-
9. Gitと他のシステム
- 9.1 クライアントとしてのGit
- 9.2 Gitへの移行
- 9.3 まとめ
-
10. Gitの内側
- 10.1 PlumbingとPorcelain
- 10.2 Gitオブジェクト
- 10.3 Gitリファレンス
- 10.4 Packfile
- 10.5 Refspec
- 10.6 転送プロトコル
- 10.7 メンテナンスとデータ復旧
- 10.8 環境変数
- 10.9 まとめ
-
A1. 付録A: 他の環境でのGit
- A1.1 グラフィカルインターフェース
- A1.2 Visual StudioでのGit
- A1.3 Visual Studio CodeでのGit
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMineでのGit
- A1.5 Sublime TextでのGit
- A1.6 BashでのGit
- A1.7 ZshでのGit
- A1.8 PowerShellでのGit
- A1.9 まとめ
-
A2. 付録B: アプリケーションへのGitの組み込み
- A2.1 コマンドラインGit
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
A3. 付録C: Gitコマンド
- A3.1 セットアップと設定
- A3.2 プロジェクトの取得と作成
- A3.3 基本的なスナップショット
- A3.4 ブランチとマージ
- A3.5 プロジェクトの共有と更新
- A3.6 検査と比較
- A3.7 デバッグ
- A3.8 パッチ適用
- A3.9 メール
- A3.10 外部システム
- A3.11 管理
- A3.12 Plumbingコマンド
7.7 Git ツール - Reset の解明
Reset の解明
より特殊なツールに進む前に、Git の reset
コマンドと checkout
コマンドについて話しましょう。これらのコマンドは、Git を初めて使うとき、最も混乱しやすい部分の 2 つです。非常に多くのことを行うため、それらを実際に理解して適切に使用することは絶望的に思えるかもしれません。このため、簡単な比喩をお勧めします。
3つのツリー
reset
と checkout
を考える簡単な方法は、Git を3つの異なるツリーのコンテンツマネージャーととらえるという精神的な枠組みです。ここで言う「ツリー」とは、厳密にはデータ構造ではなく、「ファイルの集合」という意味です。インデックスがツリーのように正確に機能しないケースもいくつかありますが、当面はそう考える方が簡単です。
Git はシステムとして、通常の操作で3つのツリーを管理および操作します。
ツリー | 役割 |
---|---|
HEAD |
最後のコミットスナップショット、次の親 |
インデックス |
次に提案されるコミットスナップショット |
ワーキングディレクトリ |
サンドボックス |
HEAD
HEAD は現在のブランチ参照へのポインタであり、これはそのブランチで最後に行われたコミットへのポインタです。つまり、HEAD は次に作成されるコミットの親になります。HEAD は、そのブランチでの最後のコミットのスナップショットと考えるのが最も簡単です。
実際、そのスナップショットがどのようなものかを見るのは非常に簡単です。HEAD スナップショット内の各ファイルの実際のディレクトリリストと SHA-1 チェックサムを取得する例を次に示します。
$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon 1301511835 -0700
committer Scott Chacon 1301511835 -0700
initial commit
$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152... README
100644 blob 8f94139338f9404f2... Rakefile
040000 tree 99f1a6d12cb4b6f19... lib
Git の cat-file
コマンドと ls-tree
コマンドは、より低レベルな目的に使用され、日常業務ではあまり使用されない「配管」コマンドですが、ここで何が起こっているかを見るのに役立ちます。
インデックス
インデックスは、次に提案されるコミットです。この概念を Git の「ステージングエリア」とも呼んでいますが、これは git commit
を実行するときに Git が参照するものです。
Git は、最後にワーキングディレクトリにチェックアウトされたすべてのファイルの内容と、最初にチェックアウトされたときの状態のリストでこのインデックスを初期化します。その後、それらのファイルの一部を新しいバージョンに置き換え、git commit
がそれを新しいコミットのツリーに変換します。
$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README
100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb
ここでも git ls-files
を使用していますが、これは舞台裏のコマンドであり、現在のインデックスがどのようなものかを示します。
インデックスは厳密にはツリー構造ではありません。実際にはフラットなマニフェストとして実装されていますが、私たちの目的には十分近いです。
ワーキングディレクトリ
最後に、ワーキングディレクトリ(一般的には「作業ツリー」とも呼ばれます)があります。他の2つのツリーは、.git
フォルダー内に効率的ですが不便な方法でコンテンツを保存します。ワーキングディレクトリはそれらを実際のファイルに展開するため、編集がはるかに簡単になります。ワーキングディレクトリはサンドボックスと見なしてください。変更をステージングエリア(インデックス)、そして履歴にコミットする前に、そこで変更を試すことができます。
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directory, 3 files
ワークフロー
Git の典型的なワークフローは、これらの3つのツリーを操作することで、プロジェクトのスナップショットを段階的に良い状態で記録することです。

このプロセスを視覚化してみましょう。単一のファイルがある新しいディレクトリに入るとします。このファイルをファイルの**v1**と呼び、青で示します。次に、git init
を実行すると、未誕生の master
ブランチを指す HEAD 参照を持つ Git リポジトリが作成されます。

この時点では、ワーキングディレクトリツリーのみがコンテンツを持っています。
次にこのファイルをコミットしたいので、git add
を使ってワーキングディレクトリのコンテンツをインデックスにコピーします。

git add
でファイルがインデックスにコピーされるその後、git commit
を実行します。これにより、インデックスの内容が永続的なスナップショットとして保存され、そのスナップショットを指すコミットオブジェクトが作成され、master
がそのコミットを指すように更新されます。

git commit
ステップgit status
を実行すると、3つのツリーすべてが同じであるため、変更はありません。
次に、そのファイルを変更してコミットしたいと思います。同じプロセスを繰り返します。まず、ワーキングディレクトリでファイルを変更します。これをファイルの**v2**と呼び、赤で示します。

今すぐ git status
を実行すると、「Changes not staged for commit」としてファイルが赤で表示されます。これは、そのエントリがインデックスとワーキングディレクトリの間で異なるためです。次に、git add
を実行してインデックスにステージングします。

この時点で git status
を実行すると、インデックスと HEAD が異なるため、「Changes to be committed」の下にファイルが緑色で表示されます。つまり、次に提案されるコミットが最後のコミットと異なっているということです。最後に、git commit
を実行してコミットを確定します。

git commit
ステップこれで git status
は何も出力しなくなります。なぜなら、3つのツリーすべてが再び同じになったからです。
ブランチの切り替えやクローンも同様のプロセスで行われます。ブランチをチェックアウトすると、**HEAD** が新しいブランチ参照を指すように変更され、そのコミットのスナップショットで**インデックス**が埋められ、次に**インデックス**の内容が**ワーキングディレクトリ**にコピーされます。
Reset の役割
reset
コマンドは、この文脈で見るとより理解しやすくなります。
これらの例のために、file.txt
を再度変更し、3回目にコミットしたとしましょう。これで履歴は次のようになります。

では、reset
を呼び出したときに何が起こるか、正確に見ていきましょう。これは、これら3つのツリーをシンプルかつ予測可能な方法で直接操作します。最大3つの基本的な操作を実行します。
ステップ1: HEAD の移動
reset
が最初に行うことは、HEAD が指すものを移動することです。これは HEAD 自体を変更する(checkout
が行うこと)のとは異なります。reset
は HEAD が指しているブランチを移動します。つまり、HEAD が master
ブランチに設定されている場合(つまり、現在 master
ブランチにいる場合)、git reset 9e5e6a4
を実行すると、まず master
が 9e5e6a4
を指すように変更されます。

コミットを伴う reset
のどの形式を呼び出しても、これは常に最初に行われることです。reset --soft
を使用すると、そこで停止します。
ここで少し時間を取ってこの図を見て、何が起こったのかを理解してください。それは本質的に最後の git commit
コマンドを取り消しました。git commit
を実行すると、Git は新しいコミットを作成し、HEAD が指すブランチをそれに移動させます。reset
で HEAD~
(HEAD の親)に戻ると、インデックスやワーキングディレクトリを変更せずにブランチを元の場所に戻すことになります。これでインデックスを更新し、再度 git commit
を実行して、git commit --amend
が行ったことを達成できます(最後のコミットの変更を参照)。
ステップ2: インデックスの更新 (--mixed
)
ここで git status
を実行すると、インデックスと新しい HEAD との差が緑色で表示されることに注意してください。
reset
が次に行うことは、HEAD が現在指しているスナップショットの内容でインデックスを更新することです。

--mixed
オプションを指定すると、reset
はこの時点で停止します。これはデフォルトでもあり、オプションをまったく指定しない場合(この場合は単に git reset HEAD~
)、コマンドはここで停止します。
ここで少し時間を取ってこの図を見て、何が起こったのかを理解してください。まだ最後の commit
は取り消されましたが、すべてが**アンステージ**されました。すべての git add
および git commit
コマンドを実行する前に戻ったことになります。
ステップ3: ワーキングディレクトリの更新 (--hard
)
reset
が行う3つ目のことは、ワーキングディレクトリをインデックスのようにすることです。--hard
オプションを使用すると、この段階まで続行されます。

では、何が起こったのか考えてみましょう。最後のコミット、git add
と git commit
コマンド、**そして**ワーキングディレクトリで行った作業すべてを取り消しました。
このフラグ (--hard
) が reset
コマンドを危険にする唯一の方法であり、Git が実際にデータを破壊する非常に数少ないケースの1つであることに注意することが重要です。他の reset
の呼び出しは非常に簡単に元に戻すことができますが、--hard
オプションはワーキングディレクトリ内のファイルを強制的に上書きするため、元に戻すことはできません。この特定のケースでは、ファイルの**v3**バージョンがまだ Git DB のコミットに存在しており、reflog
を見てそれを取り戻すことができますが、コミットしていなかった場合は、Git がファイルを上書きし、回復不能になっていたでしょう。
まとめ
reset
コマンドは、指定された順序でこれら3つのツリーを上書きし、指定した時点で停止します。
-
HEAD が指すブランチを移動する (
--soft
の場合はここで停止)。 -
インデックスを HEAD のようにする (
--hard
でない場合はここで停止)。 -
ワーキングディレクトリをインデックスのようにする。
パス付きリセット
これは reset
の基本的な動作をカバーしていますが、作用するパスを指定することもできます。パスを指定すると、reset
はステップ1をスキップし、残りのアクションを特定のファイルまたはファイルのセットに限定します。これは実際にはある程度理にかなっています。HEAD は単なるポインタであり、あるコミットの一部と別のコミットの一部を指すことはできません。しかし、インデックスとワーキングディレクトリは部分的に更新できるため、reset はステップ2と3に進みます。
したがって、git reset file.txt
を実行したと仮定します。この形式は(コミット SHA-1 やブランチを指定せず、--soft
や --hard
も指定しなかったため)git reset --mixed HEAD file.txt
の短縮形であり、次のことを行います。
-
HEAD が指すブランチを移動する (スキップ)。
-
インデックスを HEAD のようにする (ここで停止)。
つまり、実質的に file.txt
を HEAD からインデックスにコピーするだけです。

これにより、ファイルを**アンステージ**するという実用的な効果が得られます。このコマンドの図を見て、git add
が行うことを考えると、それらはまったく逆です。

これが git status
コマンドの出力がファイルをアンステージするためにこれを実行するように示唆する理由です(これについてはステージングされたファイルのアンステージを参照)。
Git に「HEAD からデータを取得する」と仮定させないで、特定のコミットからそのファイルバージョンを取得するように指定することも簡単にできます。git reset eb43bf file.txt
のように実行するだけです。

これは、ワーキングディレクトリでファイルのコンテンツを**v1**に戻し、それに git add
を実行し、その後再び**v3**に戻したかのように、実質的に同じことを行います(ただし、これらのすべての手順を実際に経るわけではありません)。今 git commit
を実行すると、ワーキングディレクトリに再度ファイルがなくても、そのファイルを**v1**に戻す変更が記録されます。
git add
と同様に、reset
コマンドも --patch
オプションを受け入れ、ハンクごとにコンテンツを選択的にアンステージまたは元に戻すことができる点も興味深いことです。
スカッシュ
この新しい力を使って、コミットをスカッシュするという興味深い方法を見てみましょう。
「oops.」、「WIP」、「forgot this file」のようなメッセージを含む一連のコミットがあるとします。reset
を使用すると、それらを単一のコミットにすばやく簡単にスカッシュして、非常に賢いように見せることができます。コミットのスカッシュでは、これを行う別の方法が示されていますが、この例では reset
を使用する方が簡単です。
最初のコミットに1つのファイルがあり、2番目のコミットで新しいファイルが追加され、最初のファイルが変更され、3番目のコミットで最初のファイルが再度変更されたプロジェクトがあるとします。2番目のコミットは作業中のものであり、それをスカッシュしたいと考えています。

git reset --soft HEAD~2
を実行して、HEAD ブランチを古いコミット(保持したい最新のコミット)に戻すことができます。

そして、もう一度 git commit
を実行するだけです。

これで、到達可能な履歴、つまりプッシュする履歴は、file-a.txt
**v1** を持つ1つのコミットがあり、次に file-a.txt
を**v3**に修正し、file-b.txt
を追加した2番目のコミットがあるように見えることがわかります。ファイルの**v2**バージョンを持つコミットは履歴にはもうありません。
チェックアウト
最後に、checkout
と reset
の違いについて疑問に思うかもしれません。reset
と同様に、checkout
も3つのツリーを操作しますが、コマンドにファイルパスを指定するかしないかで少し異なります。
パスなし
git checkout [branch]
を実行することは git reset --hard [branch]
を実行することとかなり似ており、3つのツリーすべてを [branch]
のように更新しますが、2つの重要な違いがあります。
まず、reset --hard
とは異なり、checkout
はワーキングディレクトリに安全です。変更されたファイルが失われないことを確認します。実際には、それよりも少し賢く、ワーキングディレクトリで自明なマージを試みるため、変更していないファイルはすべて更新されます。一方、reset --hard
は、チェックせずにすべてを全面的に置き換えます。
2つ目の重要な違いは、checkout
が HEAD を更新する方法です。reset
は HEAD が指すブランチを移動しますが、checkout
は HEAD 自体を移動して別のブランチを指すようにします。
たとえば、異なるコミットを指す master
と develop
ブランチがあり、現在 develop
にいるとします(そのため HEAD は develop
を指しています)。git reset master
を実行すると、develop
自体は master
と同じコミットを指すようになります。代わりに git checkout master
を実行すると、develop
は移動せず、HEAD 自体が移動します。HEAD は master
を指すようになります。
したがって、どちらの場合も HEAD をコミット A を指すように移動しますが、その方法は大きく異なります。reset
は HEAD が指すブランチを移動し、checkout
は HEAD 自体を移動します。

git checkout
と git reset
パスあり
checkout
を実行するもう1つの方法はファイルパスを使用することです。これは reset
と同様に HEAD を移動しません。git reset [branch] file
と同じように、そのコミットにあるそのファイルでインデックスを更新するだけでなく、ワーキングディレクトリ内のファイルも上書きします。これは git reset --hard [branch] file
とまったく同じです(もし reset
でそれを実行できるなら) — ワーキングディレクトリに安全ではなく、HEAD も移動しません。
また、git reset
や git add
と同様に、checkout
は --patch
オプションを受け入れ、ハンクごとにファイルコンテンツを選択的に元に戻すことができます。
まとめ
これで reset
コマンドを理解し、より快適に使えるようになったと思いますが、checkout
との違いが正確にどのようなもので、異なる呼び出しのすべてのルールを覚えることは不可能だとまだ少し混乱しているかもしれません。
どのコマンドがどのツリーに影響するかを示すチートシートを次に示します。「HEAD」列は、そのコマンドが HEAD が指す参照(ブランチ)を移動する場合は「REF」と表示され、HEAD 自体を移動する場合は「HEAD」と表示されます。「WD Safe?」列には特に注意してください。**NO** と表示されている場合は、そのコマンドを実行する前に少し考えてください。
HEAD | インデックス | 作業ディレクトリ | WD セーフ? | |
---|---|---|---|---|
コミットレベル |
||||
|
REF |
NO |
NO |
YES |
|
REF |
YES |
NO |
YES |
|
REF |
YES |
YES |
NO |
|
HEAD |
YES |
YES |
YES |
ファイルレベル |
||||
|
NO |
YES |
NO |
YES |
|
NO |
YES |
YES |
NO |