概要
- 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-cleangit-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 する場合、通常の