概要
- Git のフィルターには主に 2 つ clean フィルターと smudge フィルターがある
git add
コマンドを実行した際に clean フィルターが実行され、その標準出力へが git オブジェクトになるgit clone
を実行した際に git オブジェクトに smudge フィルターが実行され、その標準出力がローカルのファイルになる- フィルターの適用有無は
.gitattributes
ファイルにて指定され、フィルターの内容は.git/config
で指定される
動機
普段の仕事で Git LFS を扱った際に、ポインターファイルとバイナリーファイルが相互に置き換わる仕組みがわからなかったので調べた。
Git LFS
Git LFS を使うように設定すると、 .gitattributes
ファイルや .git/config
ファイルに以下のテキストが追加される
コマンド
git lfs install git lfs track '*.png'
.gitattributes
ファイル
*.png filter=lfs diff=lfs merge=lfs -text
.git/config
ファイルと gitconfig --list
コマンドの結果
.git/config
ファイル
[filter "lfs"] clean = git-lfs clean -- %f smudge = git-lfs smudge -- %f
gitconfig --list
コマンドの結果
filter.lfs.clean=git-lfs clean -- %f filter.lfs.smudge=git-lfs smudge -- %f
.gitattributes
ファイルを見ると、*.png
ファイルはすべてテキストとしては扱わず(-text
) diff する場合はgit-lfs-diff
を使い、マージのときは…同…、filter
はlfs
フィルターを使うというのがわかる。- 当の
lfs
フィルター([filter "lfs"]
)では、clean
とsmudge
フィルターが用意されていて、それぞれgit-lfs-clean
git-lfs-smudge
が指定されているので、おそらくこれがポインターファイルと実際のオブジェクトを交換するコマンドであるとわかる。
Git のフィルター
Git のフィルターについては、公式のドキュメントが詳しい。
ざっくり言えば、 次の通り。
- staging(要するに
git add
して管理状態にすること) にあるファイルにすると、 ローカルのファイル名が clean フィルターに渡されて、その標準出力の内容が staging として管理される。 - clone などで取得した場合は、 staging のオブジェクトが smudge フィルターに渡されて、その標準出力の内容が ローカルのファイルとして保存される。
そうすると、git-lfs
でなくても、自作のコマンドで動作を確かめられるのではないかと考えられる。
実験
企画
- clean フィルターに可逆な変更を加えるコマンドを設定する。
- smudge フィルターも同様に clean フィルターの変更をもとに戻すコマンドを設定する。
- テキストファイルを上記のフィルターに通すものとする。
- 上記の設定のもとレポジトリーを作成・ GitHub に push した上で、 clone して、ローカル・別ロケーションそれぞれのファイルの状態を観察する
- テキストファイルを作成し、 git 管理に(
git add
)したときに clean フィルターが適用されていることを確認する - リモートから clone してきたときに、 smudge フィルターが適用されて元のファイルになっていることを確認する
- テキストファイルを作成し、 git 管理に(
準備
.gitattributes
ファイル.git/config
ファイル
ここでは、
- テキストファイルに行番号を付与する clean フィルター
- 行番号の付与されたテキストファイルから行番号を取り除く smudge フィルター
を準備する。
1..gitattributes
ファイル
*.txt filter=example text eol=lf
2..git/config
ファイル
%f
にはファイルのパスが渡されます。- ファイルの内容は標準入力に渡されるようです。
- 標準出力への出力内容がファイルのコンテンツになるようです。
[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true [filter "example"] clean = cat -n %f smudge = sed 's|^[[:space:]]*[1-9][0-9]*[[:space:]]||g'
実験1 clean フィルター
まず適当なファイルを作成して、git 管理対象にする。
for item in {foo,bar,baz}; do echo "item = ${item}" if [[ "${item}" =~ ^ba ]]; then echo "" fi done | tee text.txt git add text.txt
ファイルの中を確認する。
cat text.txt
続いて、 hash 値から内容を確認する。ステージされたファイルは 行番号が付与されているはずである。
git ls-files --stage text.txt git cat-file [上記のコマンドの結果]
行番号が付与されていることがわかる
実験2 smudge フィルター
実験1のあとにコミットする
git commit -m 'add text'
この後に、ファイルを消してからリストアしてみる。
rm text.txt git status git restore text.txt
ファイルが復元されていることがわかる。
実験3 clone
このレポジトリーを GitHub に push してから、別のディレクトリーで clone してみる。
事前に調べた結果では、 clone してきた後のレポジトリーでは smudge フィルターのかかったコンテンツでなくて、 clean フィルターのかかった状態になっていることがわかっている。それは、 clean / smudge フィルターともに .git/config
の中で定義されているが、 clone はその設定をダウンロードできないからである(push すらされていないはず…)。
そのため、ここでは clone した場合のファイルの状態を確認してみる。
まず GitHub にプッシュする。
git remote add github https://github.com/mike-neck/my-git-filter-example.git git push github main
なお、 push されたものは行番号が付与された状態になっている。
別のディレクトリーに移動して clone して、ファイルを確認してみる。
git clone https://github.com/mike-neck/my-git-filter-example.git cd my-git-filter-example bat text.txt
見事(?)に smudge フィルターのかからない状態になっている。
ここで .git/config
を見てみると、フィルターの設定は存在しない(それはそう)ので元の状態(行番号のない状態)に戻らない。
bat .git/config
実験4 同じフィルターを導入
次に同じフィルターを clone してきた方のレポジトリーに導入してみる。 ファイルの比較は merge フィルターを指定してないので、 clean フィルター適用したもの(行番号つきファイル vs 行番号付き行番号つきファイル)になると予想。
grep -A2 'filter "example"' path/to/original-path/.git/config grep -A2 'filter "example"' path/to/original-path/.git/config >> .git/config git status
git status
の結果は意外にも変更なしだった
この状態ではリモートとローカルで同じファイルの編集ができないので、git cat-file
コマンドで復元させる。
git cat-file --filters HEAD:text.txt
git cat-file --filters HEAD:text.txt > text.txt
git status
git diff text.txt
git add text.txt
git status
ここで、 git status
で変更が発生しているように表示されるが、元のファイルと同じなので git diff
で差異は確認できない。
さらに git add
すると、 git status
は変更なしに戻った。これでフィルターのあるプロジェクトの適切な状態になったと言えるが、フィルターを設定されているリポジトリーの clone はこれらの手作業をやってくれるソフトウェアが必要であることがわかる。
実験5 fetch + pull
元のローカルレポジトリーにて新たにファイルを作成・プッシュした後に、クローンしたローカルレポジトリーで pull した場合に、 smudge フィルターが適用されているのを確認する。
cd path/to/original-path for item in {foo,bar,baz}; do echo "${item}"; done > another.txt git add another.txt git commit -m 'add another text' git push origin "$(git rev-parse --abbrev-ref HEAD)" cd pth/to/my-git-filter-example git pull origin git status
元のレポジトリー
クローンしたレポジトリーで git pull
ファイルの中身を確認→行番号はなし→smudgeフィルターが実行されている
もちろん GitHub では 行番号付きのファイルになっている。(GitHub ではsmudgeフィルターが適用されていない)
clean フィルターと smudge フィルターの仕組みを図示すると以下の図のようになる。
まとめ
- Git のフィルターには clean フィルターと smudge フィルターがある
- clean フィルターは
git add
されて Git のオブジェクトに登録されるときに適用される - smudge フィルターは git blob オブジェクトから復元した後に適用される
- clean フィルターは
- Git のフィルターは
.gitattributes
と.git/config
の組み合わせで指定する- リモートから clone する場合に、
.git/config
は同期されないので、別途準備する必要がある - clone してきた後のファイルに sumudge フィルターは適用されていないので、別途適用する必要がある
- リモートから clone する場合に、
- Git LFS は上記の仕組みを自動で適用するソフトウェアである
- レポジトリーを clone する場合、通常の
git clone
の代わりにgit lfs clone
を使う
- レポジトリーを clone する場合、通常の