こんにちは、TIG コアテクノロジーユニットの田中です。
ファイルの最終更新日時を上書きするスクリプトをGroovyで書く機会があったので紹介します。
- Groovy 4.0.0
- JVM 1.8.0_121
- Perl 5.32.1
背景
git clone/pull
した時に、ローカルにチェックアウトされたファイルの最終更新日時がどうなっているかご存知でしょうか。
答えは git clone/pull
した時刻 です。(pullした場合はコミットがあったファイルのみ最終更新日時が変わります)
この時困るのが、ファイルの最終更新日時を見てファイルの更新有無を判定し、更新があったファイルにのみ処理を実行する、いわゆる差分解析
のような事を行いたいケースです。
クローンをやり直した場合、全てのファイルの最終更新日時が変わってしまうので、結局差分解析が全てのファイルに対して走ってしまいます。pullした場合はコミットがあったファイルのみ最終更新日時が変わるので、基本的にはcloneはやり直さずpullし続ければ意図通りの差分解析を行う事は可能です。
しかし例えば、Jenkinsでスポットインスタンスを立ち上げた場合や、GitHub Actions/GitLab CIで実行した場合など、毎回クローンが必要な場合があります。
こういった状況でも差分解析を意図通りに行うため、ファイルの最終更新日時をcloneした日時ではなく、コミットした日時
である必要があります。
実はこれを実現するためのPerlスクリプトがGit公式から配布されています。今回はJVMで動かしたかったので、同様の処理を行うGroovyスクリプト
を作成しました。
Perlスクリプト
まずはPerlスクリプトを用いた方法から紹介していきます。
Git公式で配布されているPerlスクリプトはこちらにあります。
このスクリプトの探索および内容理解のため以下記事を参考にしました。
Perlスクリプトの全量は以下です。
処理の方針としては比較的単純です。
- Gitの
コミットログ
から各ファイルのコミット情報を取得 - コミットの新しい順に、対応するローカルファイルの最終更新時間をコミット時間で上書き
- コミットログで同一ファイルが出てきたら、最新のコミット時間を優先
ローカルのGit定義フォルダルートでスクリプトを実行すると、各ファイルの最終更新時間がコミット時間に変更されます。
perl git-set-file-times.pl |
#!/usr/bin/perl -w |
イメージしやすさのため、各Gitコマンドで取得されるデータ例を記載しておきます。
git ls-files -z
-z
をつけているため、ファイルはASCII NULで区切られています。<0x00>
の箇所にASCII NULが入っています。- ターミナルやコマンドプロンプトでGit ls-files -z してもNULは見えないのですが、Groovyで
'git ls-files -z'.execute().text
の実行結果をファイルに出力後、Windows版Sublime Textで確認しました。
エディタやビューアによってはNUL文字表示をサポートしていないものがあるようです。
git log -m -r --name-only --no-color --pretty=raw -z
--name-only
で更新ファイルの情報を表示します。-z
で1コミットログがASCII NULで区切られます。- Git logのオプション詳細はこちら。
<0x00>
の箇所にASCII NULが入っています。- この出力の見方は、
git ls-files
の出力の見方と同様です。
Groovyスクリプト
さて本題のGroovyスクリプトです。
処理の流れは基本的にPerlスクリプトの時と同じです。
スクリプトの全量は以下です。ローカルのGit定義フォルダルートでスクリプトを実行すると、各ファイルの最終更新時間がコミット時間に変更されます。
groovy git-set-file-times.groovy |
// gitレポジトリのファイル一覧を取得 |
以下ポイントを絞ってソースの解説をしていきます。
基本的にはJavaと同じ感覚で書く事が出来ます。
files = 'git ls-files -z'.execute().text.split("\0").collect() |
'command'.execute()
でコマンドを実行execute(null, new File(base_dir))
のように書くことで、指定したディレクトリ配下で実行できる (Jenkins等で実行する場合に有用)
text
で実行結果の文字列を取得split("\0")
で文字列をASCII NULで分割collect()
で分割した文字列をList化
// 更新日時抜き出し用 |
文字列 =~ /正規表現/
で、正規表現にマッチした文字列を探索出来るit
で正規表現文字列に一致した文字列を取得it[1]
のように指定することでグループ化した文字列を取得
if(files.remove(update_file)){ |
files.remove(update_file)
で配列filesからupdate_file
要素を削除。削除出来た場合はtrueを返す。f = new File(update_file)
で、update_file
で指定したローカルファイルを取得f.setLastModified((update_time as long) * 1000)
で、ファイルの最終更新時間を上書き。コミットログで取得したUNIX時間は10桁なので、13桁に合わせるため1000倍している。
Groovyのキャッチアップは以下のサイトを参考にしました。
処理時間の比較
それぞれのスクリプトを、23,898ファイルを持つGitプロジェクトで実行して処理時間を測定しました。対象プロジェクトの開発期間は6年程で、コミットログもそれなりに育っているという状況です。(4334コミット)
git clone/pull
の時間は含んでおらず、純粋なスクリプト実行時間のみを測定しています。Perlスクリプトのほうが速いという結果にはなりましたが、Groovyスクリプトでも2.4万ファイルに対して約5秒
と十分な性能である事が確認できました。
スクリプト | 処理時間(3回平均) |
---|---|
Groovy | 5.0 秒 |
Perl | 2.2 秒 |
処理時間はコマンドプロンプト使用、以下コマンド実行で測定しました。
powershell -C (Measure-Command {perl git-set-file-times.pl}).TotalSeconds |
powershell -C (Measure-Command {groovy git-set-file-times.groovy}).TotalSeconds |
Groovyスクリプト改良版
Git logのオプションでフォーマットを指定すると、変更に強く、かつスッキリとしたソースになります。
--pretty="--pretty=format:"update_time:%ct"
と--name-only
を指定することで、必要最小限の情報、コミット時間と更新ファイルのみを出力させる事が出来ます。--pretty
の詳細はこちらのwikiを参考にして下さい。
// レポジトリのファイル一覧を取得する |
この場合のGit log出力例は以下のようになります。
git log -m -r --name-only --no-color --pretty=format:"update_time:%ct" -z
まとめ
GroovyスクリプトはJavaと同じ感覚で書けるので、普段Javaを使っている方はほとんどキャッチアップコストをかけずに習得出来ると思います。
シェルやPerlスクリプトが少し使い難いなと思っている方にはオススメです。
コアテクノロジーユニットでは、現在チームメンバーを募集しています。
私たちと一緒にテクノロジーで設計、開発、テストの高品質・高生産性を実現する仕組みづくりをしませんか?