2019-06-19

【CI】git commit 時に自動整形 & コーディングチェックする

皆さんこんにちは。 プログラミングやコーディングを続けていくうちに、過去に自分が書いたコードの意味が分からなくなったりしたことはないでしょうか? 今回は、中〜大規模な開発をする際に重要なサーバーレスな継続的インテグレーション(CI)について書きたいと思います。

導入方法はこちら

    継続的インテグレーション(CI)とは

    WikiPedia
    継続的インテグレーション(CI)とは、ソフトウェア開発においてソースコードの品質や保守性を保ち、持続可能な開発を続けていくための方法です。 つまり、他の人が見ても理解しやすく、また自分が書いた古いコードでも処理内容が理解できるようにプログラミングしていくということですね。

    個人の趣味のスクリプトであれば CI を導入する必要はないかもしれませんが、お仕事などでのチームでの開発では重要となると思います。 また GitHub で公開するものに対しても、CI を導入するとメリットが大きいと思います。

    ソースコードの属人性(その人にしか分からないような書き方)を解消するために、開発ルールを定めることがあります。 たとえば、コーディングスタイルを定義したり(Google C++ Style Guide など)、コードレビューをするなどです。 これらは整形ツールやコーディングチェック(lint)を使って自動化することができます。

    サーバーレスで CI を導入

    CI を導入する方法として、CI サーバーを立てる方法があります。 たとえば、GitHub では Travis CI を導入することで、git commit 時に自動で CI を実行してくれます。 さらに、CI の実行結果を GitHub のバッジ として公言することができ、リポジトリの質を保証する1つの指標にできます。

    しかし、CI サービスは有料プランのものもあり(Public リポジトリは無料も)、特に社内開発ではコストになる場合があります。 そこで、CI をローカル環境に導入し、git commit 時に自動でコーディングチェックと自動整形をしたいと思います。

    pre_commit フックを使う

    git には、コミット時などにスクリプトを実行するフック機能があります。 その1つに pre_commit というものがあり、これはコミットをする直前に発動します。 よって、pre_commit に CI のプログラムを記述することで、git commit したときにコーディングチェックや自動整形を実行できます。

    自動化ツール

    コーディングチェックや自動整形を行うコマンドラインツールです。

    • clang-format C/C++ の自動整形をします。
    • cpplint C/C++ の静的コード解析をします。
    • Prettier HTML, CSS, JavaScript, PHP, MarkDown, Yaml などの自動整形をします。
    • ESLint JavaScript の静的コード解析をします。
    • phpmd PHP の静的コード解析をします。

    pre_commit 例

    私が開発するときに導入している git フックは、このリポジトリで公開しています。 blue-hood/.git_template

    このリポジトリの pre_commit を以下に貼り付けます。 試行錯誤で修正しているので、最新版とは異なる場合があります。

      #!/bin/bash
    
      cppformat="{}"
      cpplint="-whitespace/comments,-whitespace/indent"
      phpmd="cleancode,codesize,controversial,design,naming,unusedcode"
      prettier="--print-width 120"
    
      status=0
      for changed in `git diff --staged --name-only`
      do
        path=`git rev-parse --show-toplevel`/${changed}
    
        if [ -e ${path} ]; then
          basename=`basename ${changed}`
          extension=${basename##*.}
    
          case "${extension}" in
            "c"|"h"|"cpp"|"hpp" )
              colordiff -u ${path} <(clang-format -style="${cppformat}" ${path})
              clang-format -i -style="${cppformat}" ${path}
              if ! cpplint --filter=${cpplint} --quiet ${path}; then
                status=1
              fi;;
    
            "php" )
              colordiff -u ${path} <(prettier ${prettier} ${path})
              prettier --write ${prettier} ${path}
              if ! phpmd ${path} text ${phpmd}; then
                status=1
              fi;;
    
            "js"|"html"|"css"|"scss"|"yaml"|"yml"|"md" )
              colordiff -u ${path} <(prettier ${prettier} ${path})
              prettier --write ${prettier} ${path};;
    
          esac
    
          git add ${path}
        fi
      done
    
      exit ${status}
    

    git diff --staged --name-only によって、コミット予定のファイル一覧を取得しています。 そして、各ファイルの拡張子を判定して CI の処理を分岐しています。

    clang-formatprettier を使って自動整形をする際に、colordiff によって整形された箇所を表示しています。 また、cpplintphpmd のコーディングチェックに引っかかった場合は、pre_commit の終了コードを 1 にして exit しています。 このようにすることでコミットを防ぐことができます。 なお、git commit --no-verify でコミットすれば、pre_commit によるチェックをせずに強制コミットすることも可能です。

    実際に使ってみる

    pre_commit を導入して git commit すると、このように CI が実行されます。

    $ git commit
    --- /home/hato/apps/apps/app/Http/Middleware/Normalize.php	2019-05-11 21:37:46.888721101 +0900
    +++ /dev/fd/63	2019-05-11 21:38:09.399885336 +0900
    @@ -72,7 +72,9 @@
             $parsed['fragment'] = empty($parsed['fragment']) ? '' : '#' . $parsed['fragment'];
             $parsed['path'] = $dirname . $basename;
             $url = '';
    -        foreach ($elements as $element) $url .= $parsed[$element];
    +        foreach ($elements as $element) {
    +            $url .= $parsed[$element];
    +        }
             $url = $isHtmlEncoded ? htmlspecialchars($url) : $url;
             return $url;
         }
    apps/app/Http/Middleware/Normalize.php 48ms
    /home/hato/apps/apps/app/Http/Middleware/Normalize.php:16	handle accesses the super-global variable $_SERVER.
    /home/hato/apps/apps/app/Http/Middleware/Normalize.php:22	The method handle() contains an exit expression.
    

    PHP の foreach ($elements as $element) $url .= $parsed[$element]; という記述に対して、ちゃんと if 文は {} で囲むように修正してくれています。 また、$_SERVERexit() は使わないように指摘されてしまいました😅  このようにして、ソースコードの品質を保っていくことができますね。

    この記事の執筆者

    hato6502

    hata  

    中学生の頃に 6502 という CPU からプログラミングの世界に入りました。 C/C++ で1から作ることも好きですが、最近は変化の激しい IT 業界をみて Web 系にシフトしつつあります。