kanta's spike

プログラミングの学習では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つの ブランチ に枝分れしています。

stateDiagram-v2 direction RL C3 --> C2 C2 --> C1 C1 --> C0 C5 --> C4 C4 --> C2

また、コミットは複数の親を持つこともあります。 下図ではC6の親はC3C5になります。 別々の ブランチC6で1つに マージ されています。

stateDiagram-v2 direction RL C6 --> C3 C3 --> C2 C2 --> C1 C1 --> C0 C6 --> C5 C5 --> C4 C4 --> C2

このように、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 の作業例から説明します。

ブランチのチェックアウト〜編集〜コミット

  1. チェックアウト にすると、最新のコミット の内容がステージングエリア作業フォルダ に反映される
  2. ファイルを変更すると、作業フォルダ の状態が変更される
  3. git addにより、ファイルの変更内容を 作業フォルダ から ステージングエリア へ反映される
  4. git commitにより、 ステージングエリア をもとに新しいコミットが作成され、 HEAD に反映される
sequenceDiagram participant u as 利用者 participant g as git participant w as 作業フォルダ participant s as ステージングエリア participant c as 最新のコミット(HEAD) u->>g : 1. checkout main c-->>c : mainのコミットを取得 c-->>s : 最新のコミットを反映 c-->>w : 最新のコミットを反映 Note over w,c: 3種類のファイルグループが同じ内容(最新コミットの内容) u->>w : 2. ファイルを変更 Note over w: 作業フォルダの状態が変更 Note over s,c: 残りのファイルグループは同じ内容(最新コミットの内容) u->>g : 3. git add files w-->>s : 変更ファイルをステージングエリアへ反映 Note over s: 作業フォルダの内容と同じ Note over c: HEADのみは最新コミットの内容 u->>g : 4. git commit s-->>c : ステージングエリアの内容からコミットを作成 Note over c: 作業フォルダの内容と同じ Note over w,c: 3種類のファイルグループが同じ内容(最新コミットの内容)

変更を1つ前に戻す

先程の変更を元に戻したいと思います。先程のコミットにより 最新コミット(C3) が作成されたとします。

データモデルで考えると、 さきほど、編集したファイルを反映したコミットにより、メインブランチ、HEADともにC3を指すようになりました。

stateDiagram-v2 direction RL C3 --> C2 C2 --> C1 C1 --> C0 note left of C3 : HEAD note left of C3 : main

これを、1つ前の C2 に戻そうと思います。

stateDiagram-v2 direction RL C3 --> C2 C2 --> C1 C1 --> C0 note left of C2 : HEAD note left of C2 : main

いろいろな方法で元に戻せますが、今回はgit reset C2(もしくはgit reset HEAD^)で戻します。

ただし、git reset には3つのオプションがあります。

  • git reset --soft <commit>
  • git reset --miixed <commit> (--mixedgit 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’ を作成したい場合、

stateDiagram-v2 direction RL C3 --> C2 C2 --> C1 C3' --> C1 C1 --> C0 note left of C3' : 新しいHEADとmain note right of C3' : C2を削除した上で、
C3と同じ内容のコミットを作成 note left of C3 : 現在のHEADとmain

git reset --softで以下の操作をすると簡単です。

  1. チェックアウト により、3種類のファイルグループはC3の内容に
  2. git reset --soft C1により、HEADの内容をC1に変更。ただし、作業フォルダステージングエリアC3のまま
  3. git commitにより、 C3の内容をもつステージングエリア から新しいコミット C3’ が作成され、 HEAD に反映
sequenceDiagram participant u as 利用者 participant g as git participant w as 作業フォルダ participant s as ステージングエリア participant c as 最新のコミット(HEAD) u->>g : 1. checkout main(C3) c-->>c : main(C3)のコミットを取得 c-->>s : 最新のコミットを反映 c-->>w : 最新のコミットを反映 Note over w,c: 3種類のファイルグループ 全てC3に u->>g : 2。 reset --soft C1 c-->>c : C1のコミットを取得 Note over c: C1に変更 Note over w,s: C3の内容のまま u->>g : 3. git commit s-->>c : ステージングエリア(C3)の内容からコミットを作成 Note over c: C3'(C3の内容と同じ) Note over w,c: 3種類のファイルグループ 全てC3(もしくはC3')と同じ内容に

作業フォルダの変更を破棄して最新コミットの内容に戻したい

例えば、いろいろ修正した 作業フォルダ の内容を破棄して元に戻したい場合(図中 (a))、

git reset --hard HEADを実行すると、最新のコミット の状態に 作業フォルダステージングエリア も強制的に更新されます。

ただし、作業フォルダの変更が完全に失なわれるため注意して実行してください。

sequenceDiagram participant u as 利用者 participant g as git participant w as 作業フォルダ participant s as ステージングエリア participant c as 最新のコミット(HEAD) u->>g : 1. checkout main c-->>c : mainのコミットを取得 c-->>s : 最新のコミットを反映 c-->>w : 最新のコミットを反映 Note over w,c: 3種類のファイルグループが同じ内容 u->>w : 2. ファイルを変更 Note over w: 作業フォルダの状態が変更 u->>g : 3. git add files w-->>s : 変更ファイルをステージングエリアへ反映 Note over s: 作業フォルダの内容と同じ Note over u,c: (a) ファイルは変更したものの、作業フォルダの変更内容を破棄して元に戻したい u->>g : 4. git reset --hard HEAD c-->>c : 最新のコミットを取得 c-->>s : 最新のコミットを反映 c-->>w : 最新のコミットを反映(強制的に反映) Note over w,c: 3種類のファイルグループが同じ内容

参考: 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>スナップショット間の、ファイル内の差異を表示するgit diff
git checkout <revision>HEADと現在のブランチを更新するgit checkout
 
ブランチング(branching)とマージング(merging)Pro Git
への参照
git branchブランチを表示するgit branch
git branch <name>ブランチを作るgit branch
git checkout -b <name>ブランチを作り、そのブランチに切り替える
  • git branch <name>; git checkout <name>と同様
git checkout
git merge <revision>現在のブランチにマージするgit merge
git mergetoollマージコンフリクト(結合衝突)を解決するために便利なツールを使用するgit mergetool
git rebaseパッチのセットを新しいベース(土台)の上にベース(作業が完了したブランチを分岐元のブランチにくっつける)するgit rebase
 
リモートPro Git
への参照
git remoteリモートをリスト化するgit remote
git remote add <name> <url>リモートを追加するgit remote
git push <remote> <local branch>:<remote branch>リモートにオブジェクトを送信し、リモートのリファレンスを更新するgit push
git branch --set-upstream-to=<remote>/<remote branch>ローカルブランチとリモートブランチ間の通信を設定するgit branch
git fetchリモートからオブジェクトとリファレンスを回収するgit fetch
git pullgit fetch; git mergeと同様git pull
git cloneリモートからリポジトリをダウンロード(クローン)するgit clone
 
取り消し(Undo)Pro Git
への参照
git commit --amendコミットの内容やメッセージを編集する作業のやり直し
git reset HEAD <file>ファイルのステージングを解除するgit reset
git checkout -- <file>変更を破棄するパス指定ありの場合
 
Git上級者向けコマンドPro Git
への参照
git configGitをもっとカスタマイズするgit config
git clone --depth=1バージョン履歴全体を持ってこない、浅いクローン(ダウンロード)git clone
git add -p対話的なステージングパッチのステージ
git rebase -i対話的なリベース複数のコミットメッセージの変更
git blameどの行を誰が最後に編集したのかを表示するgit blame
git stash作業ディレクトリへの変更を一時的に削除するgit stach
git bisect履歴のバイナリサーチ(二分探索)を行う(ソフトウェアの後戻り、デグレードなど)git binsect
.gitignore無視するために、意図的に追跡しないファイルを指定するファイルの無視

参考

作成日: 2022/07/25