mike-neckのブログ

Java or Groovy or Swift or Golang

Git のフィルターのメモ

概要

  • 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 のフィルターについては、公式のドキュメントが詳しい。

git-scm.com

ざっくり言えば、 次の通り。

  • staging(要するに git add して管理状態にすること) にあるファイルにすると、 ローカルのファイル名が clean フィルターに渡されて、その標準出力の内容が staging として管理される。
  • clone などで取得した場合は、 staging のオブジェクトが smudge フィルターに渡されて、その標準出力の内容が ローカルのファイルとして保存される。

そうすると、git-lfs でなくても、自作のコマンドで動作を確かめられるのではないかと考えられる。

実験

企画

  • clean フィルターに可逆な変更を加えるコマンドを設定する。
  • smudge フィルターも同様に clean フィルターの変更をもとに戻すコマンドを設定する。
  • テキストファイルを上記のフィルターに通すものとする。
  • 上記の設定のもとレポジトリーを作成・ GitHub に push した上で、 clone して、ローカル・別ロケーションそれぞれのファイルの状態を観察する
    • テキストファイルを作成し、 git 管理に(git add)したときに clean フィルターが適用されていることを確認する
    • リモートから clone してきたときに、 smudge フィルターが適用されて元のファイルになっていることを確認する

準備

  1. .gitattributes ファイル
  2. .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

github.com

なお、 push されたものは行番号が付与された状態になっている。

github.com

別のディレクトリーに移動して 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フィルターが適用されていない)

github.com

clean フィルターと smudge フィルターの仕組みを図示すると以下の図のようになる。


まとめ

  • Git のフィルターには clean フィルターと smudge フィルターがある
    • clean フィルターは git add されて Git のオブジェクトに登録されるときに適用される
    • smudge フィルターは git blob オブジェクトから復元した後に適用される
  • Git のフィルターは .gitattributes と .git/config の組み合わせで指定する
    • リモートから clone する場合に、.git/config は同期されないので、別途準備する必要がある
    • clone してきた後のファイルに sumudge フィルターは適用されていないので、別途適用する必要がある
  • Git LFS は上記の仕組みを自動で適用するソフトウェアである
    • レポジトリーを clone する場合、通常の git clone の代わりに git lfs clone を使う