プログラミングの学習ではGitHubをよく使います。
GitHubを使うためにはgit
を使えないといけません。
そこで、Version Control (Git) · the missing semester of your cs education(翻訳)でgit
について学びたいと思います。
バージョン管理 (Git) の まとめ
まとめると以下になります。
- Gitのデータモデルは美しい。でも、Gitのインターフェイスは醜い
- コマンドの使い方を覚えるのではなく、データモデルを理解することが重要
- Gitはスナップショット(コミット)を 有向非巡回グラフ(DAG) で管理
- Gitのデータは、オブジェクト と リファレンス
git
は オブジェクト の追加と リファレンス の追加・更新によって、コミット の DAG を操作するツール
- コミット を作成するには ステージングエリア を使う
- コマンドの使い方はPro Gitの1〜5章を読めば、ほとんどわかるはず
MITの先生がGitのインターフェイスが醜いと言うぐらいですから、私がgit
のコマンドを覚えられないのも無理はありません。安心しました。
コマンドの使い方を覚えるより、まず、データモデルを理解することが大事です。 一度、データモデルを理解すれば、「データモデルをこのように変更するにはどうすれば良いのか?」という観点でコマンドを探し・その使い方を理解することができます。
また、個人的には、データモデル に加えて、作業フォルダ、ステージングエリア、最新のコミット(HEAD) という 3種類のファイルグループ の関係も意識する必要があると感じました。
結論としては、この講義で データモデルの概要、3種類のファイルグループ を理解してから、Pro Gitにすすみましょう。 あと、いつも悩むコミットのメッセージの書き方についても、How to Write a Git Commit Message(日本語版)という文書を紹介してくれています。この文書を読んでコミットメッセージの書き方を学びましょう。
以降では、データモデル、3種類のファイルグループについて自分用に整理します。詳しくは講義の方を参照ください。
データモデル
スナップショット(コミット)
バージョン管理システムは、日々の各時点でのファイルやフォルダの状態を保存します。 ある時点のファイルやフォルダの状態を保存したものを スナップショット と呼びます。 そして、Gitではこのスナップショットを コミット と呼びます。
スナップショットの管理方法
バージョン管理システムは、スナップショットを関連付けて履歴を管理します。 Gitでは、スナップショット(コミット)を 有向非巡回グラフ(DAG) として管理します。
図の四角形(□)がコミット(スナップショット)です。便宜上、コミットにはC0〜CN番の名前をつけています。 左から右に時系列でコミットが作成されます。
コミットは親の情報(図中の左方向の矢印)を持ちます。(注意: コミットは子の情報は持ちません。親を知るのみです。)
下図ではC3
の親はC2
になり、C4
の親もC2
になります。
C2
以降で2つの ブランチ に枝分れしています。
また、コミットは複数の親を持つこともあります。
下図ではC6
の親はC3
とC5
になります。
別々の ブランチ がC6
で1つに マージ されています。
このように、Gitではスナップショットを一直線ではなく、分岐したり、融合したりします。 これが 有向非巡回グラフ(DAG) です。
データモデル
Gitでは、ファイルはブロブ、フォルダはツリーとして管理されます。 疑似コードで書くと以下になります。 Gitはブロブとツリーとコミットで管理されるシンプルなデータモデルです。
講義で紹介されているデータモデルの疑似コードを紹介します。
// ブログはファイル。ファイルは大量のバイト
type blob = array<type>
// ツリーはフォルダ。名前付きでファイルとフォルダを含む
type tree = map<string, tree | blob>
// コミットはスナップショット。親、メタデータ、トップレベルのツリーを保持
type commit = struct {
parent: array<commit>
author: string
message: string
snapshot: tree
}
オブジェクトとアドレス管理
オブジェクトとは、ブロブ、ツリー、コミットのことです。
疑似コードで書くと以下になります。
type object = blob | tree | commit
Gitではオブジェクトを、そのSHA-1 hashをキーにして管理します。
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
SHA-1ハッシュは以下のような40文字の16進数文字列になります。
64d18464f08c1c870882ec79c86560cb367ea18e
リファレンス
SHA-1ハッシュは40文字もあり、人間向きではないので、人間用にリファレンスというコミットへのポインタを用意しています。
git
では、メインの開発ブランチの最新コミットを指す master や 常に「現在の作業場所」を指す HEAD などのリファレンスがあらかじめ用意されています。
references = map<string, string>
def update_reference(name, id):
references[name] = id
def read_reference(name):
return refrences[name]
def load_reference(name_or_id):
if name_or_id in references:
return load(references[name_or_id])
else:
return load(name_or_id)
リポジトリ
Gitのリポジトリはオブジェクトとリファレンスのことです。
git
コマンドは、オブジェクトの追加とリファレンスの追加・更新によって、コミットの有向非巡回グラフ(DAG)を操作します。
コマンドを使うときは、DAGにコマンドがどのような操作を行なっているかを意識しましょう。 逆に、DAGに対して特定の操作をするには、どのコマンドが必要なのかという観点でコマンドを選びましょう。
ステージングエリアと3種類のファイルグループ
ステージングエリア はデータモデルとは別の概念です。 ステージングエリア は、最新のコミット を作成するためのインターフェイスになります。
Gitでは、作業フォルダ の状態がそのままコミットになりません。
コミットを作成するには、含めたいファイルやフォルダを、作業フォルダで選び、ステージングエリアに追加します。
そして、git commit
するとステージングエリア から 最新のコミットを作成します。つまり、コミットはステージングエリアに追加されたものを対象にします。
git
で作業する時には、この3種類のファイルグループを意識する必要があります。
- 作業フォルダ
- ステージングエリア
- 最新のコミット
これから、この3種類のファイルグループの関係を、git
の作業例から説明します。
ブランチのチェックアウト〜編集〜コミット
- チェックアウト にすると、最新のコミット の内容がステージングエリア、作業フォルダ に反映される
- ファイルを変更すると、作業フォルダ の状態が変更される
git add
により、ファイルの変更内容を 作業フォルダ から ステージングエリア へ反映されるgit commit
により、 ステージングエリア をもとに新しいコミットが作成され、 HEAD に反映される
変更を1つ前に戻す
先程の変更を元に戻したいと思います。先程のコミットにより 最新コミット(C3) が作成されたとします。
データモデルで考えると、 さきほど、編集したファイルを反映したコミットにより、メインブランチ、HEADともにC3を指すようになりました。
これを、1つ前の C2 に戻そうと思います。
いろいろな方法で元に戻せますが、今回はgit reset C2
(もしくはgit reset HEAD^
)で戻します。
ただし、git reset
には3つのオプションがあります。
git reset --soft <commit>
git reset --miixed <commit>
(--mixed
はgit reset
のデフォルトであるため、オプションを省略可能)git reset --hard <commit>
いずれも、ブランチおよびHEADを指定した<commit>
に移動させます。
違いは3種類のファイルグループの扱い方になります。
オプション | 作業フォルダ | ステージングエリア | HEAD |
---|---|---|---|
–soft | 変更しない | 変更しない | ブランチおよびHEADを指定した <commit> に移動 |
–mixed | 変更しない | 移動した <commit> の内容に変更 | ブランチおよびHEADを指定した <commit> に移動 |
–hard | 移動した <commit> の内容に変更 | 移動した <commit> の内容に変更 | ブランチおよびHEADを指定した <commit> に移動 |
このように、git
を使う時は、データモデルに対してだけではなく、
3種類のファイルグループがどのように変更されるのかを意識する必要があります。
タイポなどの軽微な修正コミットのみ削除する
例えば、以下のようにタイポなどの軽微な修正だったC2を削除し、新たにC3と同じ内容を持つ C3’ を作成したい場合、
C3と同じ内容のコミットを作成 note left of C3 : 現在のHEADとmain
git reset --soft
で以下の操作をすると簡単です。
- チェックアウト により、3種類のファイルグループはC3の内容に
git reset --soft C1
により、HEADの内容をC1に変更。ただし、作業フォルダとステージングエリアはC3のままgit commit
により、 C3の内容をもつステージングエリア から新しいコミット C3’ が作成され、 HEAD に反映
作業フォルダの変更を破棄して最新コミットの内容に戻したい
例えば、いろいろ修正した 作業フォルダ の内容を破棄して元に戻したい場合(図中 (a))、
git reset --hard HEAD
を実行すると、最新のコミット の状態に 作業フォルダ も ステージングエリア も強制的に更新されます。
ただし、作業フォルダの変更が完全に失なわれるため注意して実行してください。
参考: git reset --hard <commit>
と git checkout <commit>
の違い
git reset --hard <commit>
と git checkout <commit>
のどちらも、
3つのファイルグループ(最新のコミット、ステージングエリア、作業フォルダ)を指定した <commit> の状態に更新します。
しかし、2点違いがあります。
コマンド | 相違点1: HEADの移動方法 | 相違点2: 作業フォルダの更新方法 |
---|---|---|
git reset --hard <commit> |
現在のブランチ と HEAD の両方を <commit> に移動 | 強制的に作業フォルダを <commit> の内容に更新 |
git checkout <commit> |
HEAD のみ <commit> に移動 | 作業フォルダが <commit> と異なる場合はマージを試みる。マージが無理な場合は、チェックアウトを中止 |
詳細は、リセットコマンド詳説 | Pro Git を参照ください。
Gitのコマンドラインインターフェイス
本講義で紹介されているGitのコマンドラインインターフェイスとPro Gitの説明を関連づけました。
コマンドの使い方の詳細はPro Gitを確認ください。
基本的なもの | Pro Git への参照 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
git help <command> | Gitコマンドについての情報を得る | git help | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
git init | .git ディレクトリ内に保存されたデータとともに、新しいGitリポジトリを作成する | git init | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
git status | 何が起っているのかを教えてくれる | git status | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
git add <filename> | ステージングエリアにファイルを追加する | git add | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
git commmit | 新しいコミットを作成する
| git commit | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
git log | 平らな履歴のログを示す | git log | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
git log --all --graph --decorate | 有向非巡回グラフ(DAG)として履歴を可視化する | git log | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
git diff <filename> | ステージングエリアに関して行なった変更を表示する | git diff | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
git diff <revision> <filename> |