エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

gitレポジトリ考古学に使う道具

こちらはエムスリーAdvent Calendar 2024 13日目の記事です。

こんにちは、エムスリーエンジニアリンググループの福林 (@fukubaya) です。

今回は、長年運用されてきたレポジトリをgitを使って発掘する上で使っている道具を紹介します。

福岡タワー(ふくおかタワー)は、福岡県福岡市早良区のシーサイドももち地区にあるランドマークタワー(電波塔)。本文には関係ありません。

帳票環境

弊社には「帳票環境」と呼ばれる、各種レポートを生成したり、調査のための環境があります。 簡単に言えばm3.comの本番サービスのデータのレプリカDBと、このDBのデータを分析したり、レポートや別のサービスで使うためのデータを生成するバッチの実行環境です。

長年運用されてきた環境ですが、オンプレ環境で維持のコストもかかるため、実行環境のクラウド移行も含めてリプレイスを進めています。 その中でも、レプリカDBが稼働しているHWの保守が2025年度末で切れるので、このタイミングまでにDBのレプリカ先をBigQueryに移行しDB自体を停止するためバッチの調査、移行を実施しています。

レポジトリからDBテーブルの使用箇所を探す

帳票環境のバッチの大半はm3reportsと呼ばれるレポジトリにあります。 m3reportsは、バッチのソースコードと実行スケジュールの設定ファイルを管理しているレポジトリで、調べてみると2013年からあります。

% git log --reverse --pretty='format:%ad'
Mon Apr 8 06:36:49 2013 +0000

他にも legacy_reporting と呼ばれるレポジトリはさらに古く2008年からありました。

% git log --reverse --pretty='format:%ad'
Wed Nov 12 09:26:19 2008 +0000

基本的には、このm3reportsを探せば帳票環境で使われているDBテーブルが分かるので、 テーブルを使っているバッチを止めたり、書き換えて別のバッチに移行させています。

スケジュール設定だけ消される

m3reportsには、バッチの本体のソースコードとバッチの実行スケジュールが書かれた設定ファイルがあります。

# 設定ファイルの例

# バッチbatch_02 月、火、水、木、金、土に実行する。batch_01が正常完了したら実行する。
batch_02    batch_01    wday=Mon,Tue,Wed,Thu,Fri,Sat    /path/to/script.sh

テーブルの名前でgit grepすれば、そのテーブルを使っているバッチを探すことができます。 しかし、長年運用されているため、使わなくなったバッチは設定ファイルからは実行スケジュールは消されますが、 不要になったバッチスクリプトだけが残っていることが多いです。 つまり、DBテーブルを使っているバッチがレポジトリにあったとしても、実際にはもうバッチは起動していないことが多いです。

DBテーブルが使われているかを正確に知るには、スケジュール設定に書かれていない、もう実行されないバッチを同定してレポジトリから消し、 今もバッチとして使われているファイルだけを残すことが必要です。

gitレポジトリ考古学

gitレポジトリを発掘し、コミットログやコメントから先人たちの意図を読み出しながら、ソースコードを整理していきます。 コミットした当人はすでにいなかったり、当時のことを覚えていなかったりするので、コミットログやコメントから意図を読みとることが必要です。 これは考古学です。

ファイルが消えたコミットを知りたい

あるバッチAが参照しているファイルBが見当たらないけど、そのバッチファイルAはすでにスケジュール実行されてなくて問題は起きていない。 バッチAを消したいけど、本当に消してよいのか(間違ってバッチAの実行が止められているだけかもしれない)を判断するため、参照先ファイルBが消えた時のコミット、コミットメッセージを確認したい時に使います。

git log -- path/to/file

これは path/to/file に関するコミットのログを出力します。ログの一番最後が削除なので1履歴に限定してもよいです。

git log -1 -- path/to/file

なお -- はコミットリビジョンなのかパスなのか曖昧になる場合に間に入れるだけで意味はないです。

git log path/to/log
fatal: ambiguous argument 'path/to/log': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

ある記述が消えた(追加された)コミットを知りたい

あるバッチAのファイルはあるが、設定ファイルからは呼び出されていない。 おそらく設定ファイルで実行スケジュールだけ消したがバッチファイルは使わないのに消されずに残っている。 それを確かめるため、設定ファイルから実行スケジュールを消したコミットを調べたい時に使います。

git log -p -S "検索対象文字列"

-S<gring> は変更(追加/削除)を検索します。 -p は実際の変更内容のdiffを表示させます。コミットだけでよければ -p は不要です。

あるテーブルが使われているバッチを探したい

「帳票環境」のレプリカDBのテーブルを停止するため、そのテーブルを使っているバッチを探し出したいです。 これは単純にgit grepを実行すればよいです。

git grep --name-only "テーブル名"

これでテーブル名が含まれるファイル名の一覧が得られるのですが、テーブル名が部分文字列になっている場合も含まれてしまいます。 "user" と検索したら "user" だけでなく "user_address""admin_user" もヒットしてしまいます。

そのため、正規表現で検索することで可能な限り正確に検索できるようにします。

git grep -E -i -e "(^|[^A-Za-z0-9_])targettext([ \"'\(\)]|$)"

-E は拡張正規表現、-i は大文字小文字を区別しない、-e は検索する文字列を正規表現で指定します。 (^|[^A-Za-z0-9_]) は行頭かテーブル名の区切りになる文字を指定しています。 ([ \"'\(\)]|$) はテーブル名の終わりか行末を指定しています。

以下のファイルを検出できるか試します。

# target
targettext
schema.targettext
export TBL="targettext"
export TBL='targettext'
select count(1) from (select x from targettext)
tbl=TARGETTEXT

# not target
tmp_targettext
targettext_1

これで部分文字列を避けてより正確に検索できます。

git grep -E -i -e "(^|[^A-Za-z0-9_])targettext([ \"'\(\)]|$)"
test.txt:2:targettext
test.txt:3:schema.targettext
test.txt:4:export TBL="targettext"
test.txt:5:export TBL='targettext'
test.txt:6:select count(1) from (select x from targettext)
test.txt:7:tbl=TARGETTEXT

git grep は -e を複数回重ねて指定できますが、これは遅いです。できるだけ正規表現で一つにまとめるようにしましょう。

# 複数回
time git grep -E -i -e "(^|[^A-Za-z0-9_])targettext " -e "(^|[^A-Za-z0-9_])targettext\"" -e "(^|[^A-Za-z0-9_])targettext'" -e "(^|[^A-Za-z0-9_])targettext\(" -e "(^|[^A-Za-z0-9_])ta
rgettext\)" -e "(^|[^A-Za-z0-9_])targettext$"
git grep -E -i -e "(^|[^A-Za-z0-9_])targettext " -e  -e  -e  -e  -e   8.96s user 0.32s system 274% cpu 3.383 total

# 1個
time git grep -E -i -e "(^|[^A-Za-z0-9_])targettext([ \"'\(\)]|$)"
git grep -E -i -e "(^|[^A-Za-z0-9_])targettext([ \"'\(\)]|$)"  1.57s user 0.40s system 295% cpu 0.667 total

大量にgit grepしたい

「帳票環境」のレプリカDBは1000以上のテーブルがあります。 これらのテーブル1つ1つに対してgit grepを実行するのには時間がかかります。 さらに、検索対象レポジトリがm3reports以外にもあって、それらも検索する となるとテーブル数×レポジトリ数だけgit grepを実行しないといけません。 そこで、大量のgit grepを実行するために、goで並行実行させることにしました。

まずは、git grepを実行する関数を作ります。

func grepRepoBase(ctx context.Context, repopath string, ops ...string) *bufio.Scanner {
    ctx, cancel := context.WithTimeout(ctx, 180*time.Second)
    defer cancel()
    args := append([]string{"--no-pager",
        "-c", "core.quotepath=false",
        "-c", "grep.linenumer=true",
        "-c", "grep.patternType=fixed",
        "-C", repopath,
        "grep", "--no-color", "--line-number", "-E", "-i"}, ops...)

    cmd := exec.CommandContext(ctx, "git", args...)
    buf := &bytes.Buffer{}
    cmd.Stdout = buf
    cmd.Run()

    return bufio.NewScanner(buf)
}

repopath はレポジトリがあるファイルパスです。-C で指定することで、レポジトリの外のディテクトリからでも実行できます。 出力結果を1行ずつ読み込むためbufio.Scanner を返します。 また、並行処理するためcontext.Context を使ってタイムアウトを設定しています。

// tableName を検索
s := grepRepoBase(context.Background(), repopath, "-e", fmt.Sprintf("(^|[^a-zA-Z0-9_])%s([ \r\t'\"\\)\\(]|$)", tableName))
for s.Scan() {
    line := ReadBytes(s.Bytes())
    ...
}

これをsemaphoを使って同時実行数を50までに抑えながら並行して実行します。

    // 結果を書くためのmutex
  var resultMutex sync.Mutex

    // 並列実行
    sem := semaphore.NewWeighted(50)
    wg := sync.WaitGroup{}

    logger.Info("start check")
    for _, table := range tables {
        for _, repo := range REPO_PATHS {
            if err := sem.Acquire(context.Background(), 1); err != nil {
                panic(err)
            }
            wg.Add(1)
            go func(table string, repo string) {
                now := time.Now()
                defer sem.Release(1)
                defer wg.Done()

                // ここでgit grep実行

                resultMutex.Lock()
                // 結果記録
                resultMutex.Unlock()

                logger.Info("check done",
                    "table", table, "repo", repo, "time(ms)", time.Since(now).Milliseconds())
            }(table, repo)
        }
    }
    wg.Wait()
    logger.Info("check done")

これで大量のgit grepを並行して実行できます。

git grepの結果がEUC-JPでもUTF-8でも正しく日本語を出力する

m3reportsを含むレポジトリは10年以上前からあるため、文字コードが混在しています。 古いバッチはEUC-JPで書かれていることが多いですが、最近作られたものはUTF-8で書かれています。 git grepの出力は元の文字コードに関係なく出力されるので、文字コードを固定にしてデコードすると化けてしまいます。 そこで、EUC-JPでデコードしてエラーだったらUTF-8でデコードする処理を入れています。 幸いShift-JISが使われていることはないので、EUC-JPだけをチェックすればよいです。

func ReadBytes(b []byte) string {
    // UTF-8としてデコードできればそのまま返す
    if utf8.Valid(b) {
        return string(b)
    }

    // EUC-JPでデコードしてみる
    var tmpbuf bytes.Buffer
    ic := transform.NewWriter(&tmpbuf, japanese.EUCJP.NewDecoder())
    _, err := ic.Write(b)

    // エラーなら空文字を返しておく
    if err != nil {
        return ""
    }
    err = ic.Close()
    if err != nil {
        return ""
    }

    // デコードした文字列
    return tmpbuf.String()
}

まとめ

「帳票環境」のリプレイスのため、10年もののgitレポジトリを発掘するための道具を紹介しました。

  • ファイルが消えたコミットを探す
  • ある記述が消えた(追加された)コミットを探す
  • あるテーブルが使われているバッチを探す
  • 大量にgit grepを実行
  • EUC-JPでもUTF-8でも正しく日本語を出力する

We are hiring!

10年ものから先月できたもの、もちろんこれからできる新たなものまで多様なサービスを一緒に開発してくれる仲間を募集中です。お気軽にお問い合わせください。

jobs.m3.com