Help us understand the problem. What is going on with this article?

Git LFS をちょっと詳しく

More than 1 year has passed since last update.

Git LFS の機能が実際にどういう感じで動作しているかを、基本的な Git の手順に沿って少しだけ詳しく調べてみました。

なお、ロック機能については検証していません orz
(今後に期待)

TL;DR

  • ワークツリーの情報をリポジトリ(.git/)に格納するタイミング(clean filter)で対象のファイルがメタ情報(ポインタ)に置き換えられ、ファイルの実体(オブジェクト)は .git/lfs/ 以下に格納される
  • push の直前に LFS API を通してオブジェクトがサーバーにアップロードされる
  • リポジトリ(.git/)からワークツリーに展開するタイミング(smudge filter)でメタ情報から実体ファイルに置き換えられる
  • LFS オブジェクトが .git/lfs/ 以下にない場合は LFS API を通してサーバーからダウンロードされる
  • コミット時、マージ時、チェックアウト時にそれぞれロック状態をチェックし、その後の処理をどうするかが判断される

Git LFS とは

Git はファイルの変更差分ではなく、その時点のスナップショットをとることでバージョンの管理を行うため、動画やグラフィックス用の大きなファイルを扱うには不向きです。

そこで GitHub が中心となって大きなファイルを扱うための拡張機能を作成しました。
Git LFS は、それらのファイルのメタ情報だけを Git で管理し、ファイルの実体はリモートサーバーで一元管理する仕組みを提供しています。

ゲーム開発などの現場では、ソースコードは Git で管理し、アセットデータは Subversion で管理する、など、複数のバージョン管理システムを利用していることもあると思います。
このような場合、ソースコードとアセットで管理が別になることで、手間が増えたり管理が煩雑になったりすることもあります。

Git LFS を使うことで、プロジェクトのバージョン管理を Git に統合でき、ソースコードもアセットデータも含めたシステム全体を、例えば GitHub を中心としたエコシステムのワークフローに乗せることができるかもしれません。

クライアントの動作

Git LFS の初期化

Git と Git LFS はインストール済みとします。
以下のバージョンで確認しました。

$ git version
git version 2.20.0

$ git lfs version
git-lfs/2.6.1 (GitHub; darwin amd64; go 1.11.2)

まずはじめに Git のローカルリポジトリを作成します。

$ git init
Initialized empty Git repository in /path/to/git/repo/.git/

この時点でのリポジトリの構成を見てみます。

$ tree -a
.
└── .git
    ├── HEAD
    ├── config
    ├── description
    ├── hooks
    │   ├── applypatch-msg.sample
    │   ├── commit-msg.sample
    │   ├── fsmonitor-watchman.sample
    │   ├── post-update.sample
    │   ├── pre-applypatch.sample
    │   ├── pre-commit.sample
    │   ├── pre-push.sample
    │   ├── pre-rebase.sample
    │   ├── pre-receive.sample
    │   ├── prepare-commit-msg.sample
    │   └── update.sample
    ├── info
    │   └── exclude
    ├── objects
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags

ほとんどが空のディレクトリで、 hooks/ にいくつかのサンプルスクリプトがあるだけです。

次に、LFS を初期化します。
初期化するには git lfs install コマンドを実行します。

$ git lfs install
Updated git hooks.
Git LFS initialized.

では、リポジトリがどう変わったかを見てみます。

$ tree -a
.
└── .git
    ├── HEAD
    ├── config
    ├── description
    ├── hooks
    │   ├── applypatch-msg.sample
    │   ├── commit-msg.sample
    │   ├── fsmonitor-watchman.sample
    │   ├── post-checkout                // <- 追加された
    │   ├── post-commit                  // <- 追加された
    │   ├── post-merge                   // <- 追加された
    │   ├── post-update.sample
    │   ├── pre-applypatch.sample
    │   ├── pre-commit.sample
    │   ├── pre-push                     // <- 追加された
    │   ├── pre-push.sample
    │   ├── pre-rebase.sample
    │   ├── pre-receive.sample
    │   ├── prepare-commit-msg.sample
    │   └── update.sample
    ├── info
    │   └── exclude
    ├── objects
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags

以下のフックスクリプト1が追加されました。
- post-checkout
- post-commit
- post-merge
- pre-push

試しに post-checkout の中身をチェックしてみます。

$ cat .git/hooks/post-checkout
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-checkout.\n"; exit 2; }
git lfs post-checkout "$@"

LFS コマンドのチェックをして、コマンドがあれば git lfs post-checkout コマンドを実行するようになっています。
他の3つのスクリプトに関しても同様に、対応する git lfs コマンドを実行するスクリプトになっています。

また、ここで git config を確認すると、以下のフィルタが追加されていることがわかります。
これは、ユーザの .gitconfig に追記されています。

$ git config -l
~
filter.lfs.clean=git-lfs clean -- %f
filter.lfs.smudge=git-lfs smudge -- %f
filter.lfs.process=git-lfs filter-process
filter.lfs.required=true
~

// "%f" は実行時に対象のファイルパスに置き換えられる

つまり、
- git add 時に clean フィルタで git lfs clean が実行される
- git checkout 時に smudge フィルタで git lfs smudge が実行される
- git checkout の直後に post-checkout フックで git lfs post-checkout が実行される
- git commit の直後に post-commit フックで git lfs post-commit が実行される
- git push の直前に pre-push フックで git lfs pre-push が実行される
- git merge の直後に post-merge フックで git lfs post-merge が実行される

ことになります。

最後に、実際に画像ファイルを LFS で管理する下準備として、ファイルの拡張子を登録します。

$ git lfs track "*.jpg"
Tracking "*.jpg"

.gitattributes ファイルが作成され、以下のような属性2が追加されます。

$ cat .gitattributes
*.jpg filter=lfs diff=lfs merge=lfs -text

これで LFS を使う準備が整いました。

ファイルを追加する

画像ファイルを追加してみます。

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    sample01.jpg

nothing added to commit but untracked files present (use "git add" to track)

sample01.jpg という画像ファイルをワークツリーに追加しました。
この時点ではリポジトリ(.git/)に変更はありません。

git add を実行して追加したファイルをステージングにあげてみます。

$ git add sample01.jpg

見た目は何も変わりません。
リポジトリの状態を見てみます。(一部省略して表示しています)

$ tree -a
.
├── .git
│   ├── COMMIT_EDITMSG
│   ├── lfs
│   │   ├── objects
│   │   │   └── 44
│   │   │       └── a8
│   │   │           └── 44a8486abdf0330a3fe6b586e407506e738fe075fe0b5dfc43961e358fda7206
│   │   └── tmp
│   ├── logs
│   │   ├── HEAD
│   │   └── refs
│   │       └── heads
│   │           └── master
│   ├── objects
│   │   ├── 89
│   │       └── 2dbc51ea44a89f4619397b3d0ce8d939504901
│   └── refs
├── .gitattributes
├── README.md
└── sample01.jpg

.git/lfs/ ディレクトリが作成され、その中にオブジェクトが格納されています。
このファイルが画像ファイルの実体になります。

また、.git/objects/ 以下に新しく作成されたファイル(89/2dbc...)を見ると先程の .git/lfs/ 以下のオブジェクトファイルのハッシュ値が格納されていることがわかります。

$ git cat-file -p 892dbc
version https://git-lfs.github.com/spec/v1
oid sha256:44a8486abdf0330a3fe6b586e407506e738fe075fe0b5dfc43961e358fda7206
size 2684586

つまり、clean フィルタで実行された git lfs clean によって、ファイルの実体が .git/lfs/ 以下に格納され、Git で管理されるファイルにはそのオブジェクトハッシュとファイルサイズの情報に置き換えられたことになります。

では、そのままコミットしてみます。

$ git commit -m "add sample file"
[master 67712b1] add sample file
 1 file changed, 3 insertions(+)
 create mode 100644 sample01.jpg

ここで post-commit はファイルのロック状態をチェックしています。今はロックの設定をおこなっていないので、そのままスルーしてコミットが完了しました。

プッシュする

$ git push -u origin master
origin git@github.com:ikmski/git-lfs-test.git
Uploading LFS objects: 100% (1/1), 2.7 MB | 255 KB/s, done
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 4 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (9/9), 821 bytes | 821.00 KiB/s, done.
Total 9 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To github.com:ikmski/git-lfs-test.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

pre-push フックでリモートサーバーに Git LFS API を使ってオブジェクトをアップロードされました。

Git LFS API については後述します。

リモートから取得する

新しいディレクトリを作成、Git を初期化し、先程の GitHub リポジトリをリモートに登録します。

$ git init
$ git lfs install
$ git remote add origin git@github.com:ikmski/git-lfs-test.git

$ git fetch
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (5/5), done.
Unpacking objects: 100% (9/9), done.
remote: Total 9 (delta 1), reused 9 (delta 1), pack-reused 0
From github.com:ikmski/git-lfs-test
 * [new branch]      master     -> origin/master

リポジトリ内を確認してみます。

$ tree -a
.
└── .git
    ├── FETCH_HEAD
    ├── HEAD
    ├── config
    ├── description
    ├── hooks
    ├── info
    │   └── exclude
    ├── logs
    │   └── refs
    │       └── remotes
    │           └── origin
    │               └── master
    ├── objects
    └── refs
        ├── heads
        ├── remotes
        │   └── origin
        │       └── master
        └── tags

この時点では LFS で管理しているファイルは同期されていません。

では origin/master をマージしてみます。

$ git merge origin/master

標準出力には特に何も表示されませんでしたが、tcpdump などで見てみると、
1. smudge フィルタにより git lfs smudge が実行される
2. 画像ファイルの実体がダウンロードされ、.git/lfs/ 以下に格納される
3. sample01.jpg ファイルとして作業ディレクトリにコピー

という処理が行われているようです。
その後、post-merge フックスクリプトでファイルのロック状態をチェックしています。

git checkout 時も同じように、smudge フィルタにより、Git で管理しているポインタファイルをもとに、作業ディレクトリに .git/lfs/ 以下の実体のファイルをコピーします。

また、post-checkout フックでもファイルのロック状態をチェックしています。

Git LFS API

Git LFS API には
- Batch API
- File Locking API

があります。

Batch API

batch API は LFS オブジェクトの転送指示を要求する API で、
clean フィルタで変換したオブジェクトのメタ情報(oid と size)を JSON で POST すると
実体のファイルのアップロード/ダウンロード用の URL を返します。

クライアントは、その URL に対してオブジェクトをアップロード/ダウンロードします。

File Locking API

File Locking API は Git LFS の v2.0 で追加されました。
LFS オブジェクトに対してロックを掛けたり解除したりする機能を提供します。

クライアントは各フックスクリプトでロックの状態を確認し、その後の処理を実行するかを判断します。

参考資料


  1. .git/hooks/ ディレクトリにスクリプトを置くことで、特定のアクションが発生したタイミングでそのスクリプトを実行することができる。 

  2. 個別のファイルやパスに対して属性(attribute)の設定を追加することで、そのファイルに対して個別の処理を行うことができます。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした