Git LFSについて調べてみた

Kota Tsuyuzaki
nttlabs
Published in
10 min readOct 30, 2019

こんにちは。NTT研究所の露崎です。

業務でGitの一機能であるGitLFSの導入の仕方を調べてたのですが日本語のドキュメントでちょっと欲しい情報が見つからなかったので本ブログにまとめました。

Git LFSとは

詳細な話は "git lfs”でGoogle検索かけると最初にヒットするこちらなどが参考になるのでここでは省きますが、簡単にいうと「画像ファイルなどのgitで管理されるべきでないとされるバイナリファイルなどを、まとめてgit上で管理するため機能」です。通常、gitは差分を管理しており各クライアントはその差分をGitHubなどのgitリポジトリサーバと共有しています。これに対して、git lfs機能を経由して追加されたファイルはファイルの実体は別のオブジェクトストレージ(図のLarge File Storage)などに保存されそのハッシュ値、保存先だけがgitリポジトリで管理されます。こうすることで、git上ではファイルに関する情報を管理するだけでよくなります。また、実際にコミットをチェックアウトする際には、現在のコミットに含まれるバイナリファイルのみをLarge File Storageから取ってくればよくなるためクライアントのディスク消費を抑えることができるようになる、というメリットがあります。

git lfsのイメージ (出典: https://git-lfs.github.com/ )

独自パッケージをまとめて管理したい

私のグループでは別のブログで紹介した通り、クラスタ管理用のAnsibleをGitHub上で管理し、AWS CodeBuildを使ってCIを回すことでコードの品質担保を目指しています。この仕組みは外部のリポジトリサイトで配布されている物をダウンロードしてデプロイする程度では十分だったのですが、機能の拡充などで自分たちでスクラッチで書いたコードやそれをパッケージ化したものを配布するのに不便でした。パッケージ化したファイル、例えばDebianの .deb パッケージなどはgitで扱うのに不向きなファイルだったためです。こうしたファイルの置き場としてAmazon S3など別のファイル置き場を用意して管理する方法も検討もしたのですが、公開範囲やバージョン管理が複雑になりがちなので、シンプルに同じリポジトリでまとめて管理できれば楽だなと考えたのが検討を始めたきっかけでした。

安全に導入できるか

導入にあたって気になったのが 「複数人で開発していてコードも存在するリポジトリにGit LFSを適用する」方法で、これが冒頭で書いた「欲しい情報」です。Git LFSはその性質上、gitクライアント側で機能をオン/オフする必要があるので、

  • Git LFSを導入していない人がGit LFSを有効化したリポジトリをpullしたらどうなるか
  • Git LFSを導入していない人が誤ってバイナリファイルをpushする心配はないのか

という懸念があります。一方で他の解説記事では仕組みや単一ユーザでのマイグレーションの仕組みは丁寧に解説されているのですが、この辺の仕組みがよくわからなかったので自分で触って調べてみた、という話です。

Git LFSのhookはどこからくるの?

他の記事などで解説されていますが、Git LFSを導入すると .git/hookspre-push などのスクリプトが配置され、このhookスクリプトによってgit-lfs が導入されているかどうか判定を実行できます。Git LFSを有効化したリポジトリでこのhookが機能するのであれば、これだけで懸念事項は解決できそうなので、試しに遊び用のリポジトリを作って挙動を確認してみました。既存のリポジトリにgit-lfsを追加する手順はこんな感じです。

git clone https://github.com/bloodeagle40234/play-with-git-lfs
brew install git-lfs
cd play-with-git-lfs
git-lfs install

git-lfs は有効にしただけで勝手にバイナリファイルを自動識別してくれるわけではないので git-lfs の機能を有効にしたいファイルの拡張子を設定します。

git-lfs track "*.png"

これを実行するとリポジトリ直下の .gitattributes

*.png filter=lfs diff=lfs merge=lfs -text

というエントリが追加され、リポジトリ配下のpngファイルをトラックできるようになります。また .git/hooks 配下の pre-push を見ると

#!/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/pre-push.\n"; exit 2; }
git lfs pre-push "$@"

ちゃんとhook用のスクリプトが配置されていることがわかります。さて、PNGファイルを追加してpushした後、git-lfsをuninstallした端末でcloneを試してみます。

$ git clone https://github.com/bloodeagle40234/play-with-git-lfs
Cloning into 'play-with-git-lfs'...
remote: Enumerating objects: 13, done.
remote: Total 13 (delta 0), reused 0 (delta 0), pack-reused 13
Unpacking objects: 100% (13/13), done.
git-lfs filter-process: git-lfs: command not found
fatal: The remote end hung up unexpectedly
warning: Clone succeeded, but checkout failed.
You can inspect what was checked out with 'git status'
and retry the checkout with 'git checkout -f HEAD'

お、スクリプトとちょっとメッセージは違いますが、ちゃんと失敗しましたね。念のため、別の端末でも実行してみましょう。

$ git clone https://github.com/bloodeagle40234/play-with-git-lfs
Cloning into 'play-with-git-lfs'...
remote: Enumerating objects: 13, done.
remote: Total 13 (delta 0), reused 0 (delta 0), pack-reused 13
Unpacking objects: 100% (13/13), done.
$ git-lfs # check if the command is available
-bash: /usr/local/bin/git-lfs: No such file or director

あれれ、 git-lfs が無いのに成功してしまいました。 .git/hooks にもhookスクリプトが配置されていません。これが成功してしまうようだと git-lfs を入れ忘れた開発者がいるとGit LFSでトラックされていないファイルが存在する可能性が出てきてしまいます。では、これらのhookはどこから来たのでしょうか。

Git LFSはgitconfigで管理されている

調べたところ両者の環境の違いは $HOME/.gitconfig のエントリでした。lfsのhookによる検査が成功した環境では以下のエントリが存在しています。

[filter "lfs"]
process = git-lfs filter-process
required = true
clean = git-lfs clean -- %f
smudge = git-lfs smudge -- %f

git-lfs installマニュアルに ”Set up the clean and smudge filters under the name lfs” in the global Git config” とあるように git-lfs install がこのフィルタエントリを追加しています。そして、 このエントリがあればgit-lfs がuninstallされた後にもgitの各種操作のトリガとして動作します。最初のcloneで git-lfs filte-process: git-lfs: command not found と言われた理由はこれですね。ユーザの git clone実行時に git-lfs filter-processのフィルタがかかったのですが git-lfsがuninstallされていたので command not foundで失敗したということです。つまり、この実験によって、このフィルタエントリが存在しない場合には通常通りの git clone が実施される、ということがわかりました。言い換えると、このhookを用いた仕掛けで git-lfs の有効化を保証することはできないということです。

Untracked LFS filesを検出する

git-lfs ではクライアント側で一度でも git-lfs install を実行していないとhookやfilterによるGit LFS利用の保証ができないことがわかりましたが、そうなるとチームメンバ全員が一斉にGit LFSを有効化をしなければいけません。また、このリポジトリではGit LFSが必要だということの周知が漏れたりすると新規チームメンバがGit LFSの機能なしでバイナリファイルを追加される可能性があるので、運用で対処するというのはあまり現実的ではありません。

これを解決する仕組みを調べたところ git-lfs ls-files でトラックされているファイルの一覧を取得できるのがわかったので、以下のようなコードでCI上でチェックすることにしました。

tracked fileとtrackされるべきファイルの一覧を比較するテスト

もう少しキレイなコードでも書ける気がしますがこのテストでやってることは

  1. リポジトリ内で.gitattributes に定義されたファイル名のパターンにマッチするファイルのリストを取得する
  2. git-lfs ls-files でトラックされているファイルのリストを取得する
  3. 1と2の結果にdiffがあれば失敗する

という単純なことです。 git checkattr とかを使えばもう少しキレイに書けそうではあるのですがsubprocessで起動すると今のコードより10倍以上検査が遅くなったのでやめました。簡単なチェックですが、これをCIで実行しておくことで Git LFSをクライアントに導入していない開発者からパッチがpushされたとしてもCIが失敗しそのようなコミットをmasterにマージすることを防ぐことができます。またCIで検出されることで未設定のユーザが設定し忘れていることを認識するきっかけを作ることもできると言えます。

まとめ

Gitでカジュアルにバイナリファイルを管理するためのGit LFSについて

  • Git LFSはGitHubなどでバイナリファイルを管理する仕組みとして便利
  • Git LFSのフィルタは有効だがクライアントで設定する必要がある
  • 設定し忘れなどの運用ミスを防ぐためにはCIなどの工夫が必要

といった点を紹介しました。

おわりに

私たちNTTは、オープンソースコミュニティで共に活動する仲間を募集しています。ぜひ弊社 ソフトウェアイノベーションセンタ紹介ページ及び、採用情報ページをご覧ください。

--

--

Kota Tsuyuzaki
nttlabs
Writer for

Research Engineer at Nippon Telegraph and Telephone Corporation (NTT). OpenStack Swift core team member, OpenStack Storlets Project Team Lead.