前回の記事で、AI と書いた文章の 93.2% が残っていたと書きました。AI と書いたコミットが .md に足した行のうち、測った時点でまだ残っている行の割合です。そのとき、空白の修正や行の移動があると、内容が同じでも git blame は元のコミットの行として数えない、と断り書きをしていました。ずっと気になっていたので、同じ時点のリポジトリで数え直しました。

分かったことは 4 つです。

  • 日本語の文書で行の移動を追うのに、git blame の -M と -C は使えません。Git は移した行のかたまりに含まれる英数字の数で移動かどうかを決めていて、日本語の文字は数えないからです
  • 移動を見逃していないかは、消えたと数えた行と同じ文面が、いまもどこかにあるかで確かめられます。前回の数字では、全部を移動と見なしても 93.2% が 93.4% になるだけでした
  • 残存率を数えるなら、-C は付けないほうが数字の意味がはっきりします。移した行は元の書き手に返るのに分母の追加行数はそのままなので、92.8% にかえって下がりました
  • 残存率を比べるなら、書いてから測るまでの期間を揃えます。同じ 7 月のコミットを 2 か月後に数えると、93.2% が 57.4% まで下がっていました

過去の時点を指定して数え直す

対象は前回と同じ、AI エージェントと書いている文書のリポジトリです(private)。前回の計測時点の最新コミット(2026-07-30 16:36 の a256f7f)を探し、その時点で数え直しました。

数えるのに使ったのは survival.py で、数えたいリポジトリの中で実行します。前回の記事のコマンドと同じく、Co-Authored-By: に Claude が入ったコミットが .md に足した行を数えます。加えて、git blame で消えたと出た行を、文面が残っているかで分けます。数えるコミットの範囲と、測る時点を別々に指定できます。

python3 survival.py                  # HEAD までのコミットを、HEAD で数える
python3 survival.py a256f7f          # a256f7f までのコミットを、その時点で数える
python3 survival.py a256f7f HEAD     # 同じコミットを、いまの状態で数える

Claude 以外の AI と書いているなら "claude" を、.md 以外のファイルを数えるなら ".md" と "*.md" を書き換えてください。リポジトリを読むだけで、何も変更しません。

前回の計測時点で数えた結果です。

$ python3 survival.py a256f7f
2026-07-16  追加   5849  残存   5076   86.8%
2026-07-17  追加   1886  残存   1718   91.1%
2026-07-19  追加    236  残存    216   91.5%
2026-07-20  追加   3903  残存   3834   98.2%
2026-07-21  追加   1176  残存   1137   96.7%
2026-07-22  追加    863  残存    791   91.7%
2026-07-24  追加    239  残存    153   64.0%
2026-07-25  追加   1103  残存   1047   94.9%
2026-07-26  追加   2851  残存   2620   91.9%
2026-07-27  追加   2370  残存   2357   99.5%
2026-07-29  追加   1638  残存   1618   98.8%
2026-07-30  追加    720  残存    717   99.6%
合計  追加 22834  残存 21284  93.2%
消えた 1550 行のうち 文面も残っていない 1243、空行 235、文字のある行 44、記号だけの行 28

日ごとの数字も、合計の 93.2% も、前回の記事と同じでした。最後の行が、今回足した内訳です。

blame のオプションで、残存率はどれだけ変わるか

この節の数字は survival.py ではなく、前回の記事のコマンドで出しました。2 本目の git blame に -w などを足します。

git ls-files '*.md' | xargs -I{} git blame -w --line-porcelain HEAD -- {} 2>/dev/null \
 | awk '/^[0-9a-f]{40} /{print $1}' | sort | uniq -c \
 | awk '{print $2, $1}' | sort > alive.txt

その時点の .md は全部で 26,777 行です。オプションごとの残存率と、オプションなしの結果と行ごとに比べて帰属するコミットが変わった行の数を並べます。行ごとに比べたスクリプトは載せていません。

オプション Git の説明 帰属が変わった行 残存率
なし 93.2%
-w 空白の違いを無視する 8 93.2%
-M 同じファイルの中で移動・コピーした行を探す 2 93.2%
-C 同じコミットで変更した、別のファイルからも探す 291 92.8%
-C -C ファイルを作ったコミットの、別のファイルからも探す 2,092 90.9%
-C -C -C すべてのコミットの、別のファイルからも探す 2,172 90.8%

Git の説明は git-blame のドキュメント(2026-09-25 取得)を要約しました。使った Git は 2.55.0 です。

-w と -M で帰属が変わった 8 行と 2 行は、どれも AI と書いたコミットの間で移っただけで、合計は変わりませんでした。空白だけの修正も、同じファイルの中の移動も、ほとんど無かったように見えます。ただ、-M と -C が日本語の行の移動をそもそも見つけられるかは、別に確かめる必要がありました。

日本語だけの行は、-M と -C で移動と見なされない

英語と日本語で 4 つを試しました。3 行ずつのブロックを 2 つ入れ替える、3 行を別のファイルへ移す、行頭に空白を足す、英数字の多いコマンドの 1 行と一緒に 3 行を別のファイルへ移す、です。blame-move-ja.sh は一時ディレクトリに小さなリポジトリを作るだけで、ほかの場所は変更しません。

$ sh blame-move-ja.sh
英語: 1 つ目のコミットに帰属する行
  入れ替えた 6 行                  なし 3  -M 6
  別のファイルへ移した 3 行        なし 0  -C 3
  行頭に空白を足した 3 行          なし 0  -w 3
  コマンドの行と一緒に移した 3 行  なし 0  -C 3
日本語: 1 つ目のコミットに帰属する行
  入れ替えた 6 行                  なし 3  -M 3
  別のファイルへ移した 3 行        なし 0  -C 0
  行頭に空白を足した 3 行          なし 0  -w 3
  コマンドの行と一緒に移した 3 行  なし 0  -C 3

入れ替えでは、オプションなしでも片方のブロックの 3 行は最初のコミットに残ります。差分の上では、片方だけが動いたように見えるからです。英語の行は、-M と -C を付けるとすべて最初のコミットに戻りました。日本語の行が戻ったのは、空白を無視する -w と、コマンドの行と一緒に移したときだけです。

-M と -C は、移動と見なすのに英数字の数の下限を置いています。既定は -M が 20、-C が 40 です(git-blame のドキュメント)。英数字の数え方は Git 2.55.0 のソースで確かめました。数えるのは連続して動いた行のかたまり全体で(blame.c の blame_entry_score)、英数字かどうかを決める Git 自前の表(ctype.c)には 128 以上のバイトの項目がありません。UTF-8 の日本語は 128 以上のバイトだけでできているので、日本語だけのかたまりは何行あっても下限に届きません。

かたまり全体で数えるので、英数字の多い行と一緒に動けば日本語の行も戻ります。前回の計測時点でも、-C で帰属が変わった 291 行のうち 85 行は、英数字を 1 つも含まない行でした。

残存率を数えるときは -C を付けない

-C で帰属が変わった 291 行のうち、112 行は AI と書いたコミットから、Claude が共同作成者に入っていないコミットへ移りました。AI と書いたコミットが別のファイルから持ってきた行を、素の git blame は AI と書いた行として数えていたわけです。-C は、それを元の書き手に返します。

一方、分母では、持ってきた行も追加した行として数えたままです。分子だけが減って、残存率が下がります。

-C -C では、残っている行が追加した行より多いコミットが 11 件(超過分は合わせて 797 行)出ました。-C は移動だけでなくコピーも元のコミットに返します。同じ文を何か所かに写していれば、写した先の行も元のコミットの分に数えられ、追加した行数を超えることがあります。

残存率に -C を付けるなら、分母からも移動とコピーを除く必要があります。そこまでしないなら、付けずに数えたほうが数字の意味がはっきりします。

消えた行の文面が、まだどこかにあるかを数える

-M と -C では日本語の移動を追えないので、帰属を使わずに確かめました。git blame で消えたと出た行について、同じ文面の行が、測る時点の .md のどこかにあるかを見ます。前後の空白を除いた完全一致で比べるので、少し書き換えてから移した行は「文面もどこにも残っていない」に入ります。survival.py の出力の最後の行がこれです。

前回の計測時点で消えた 1,550 行は、次のように分かれました。

分類 行数
文面もどこにも残っていない 1,243
空行 235
記号だけの行(表の区切りなど) 28
文字のある行で、同じ文面がどこかにある 44

空行と記号だけの行は、どの文書にも同じものがあるので、見つかっても移動したとは言えません。移動したかもしれないのは文字のある 44 行だけで、これもたまたま同じ文面が別の場所にあるだけかもしれません。全部を移動と見なして残っている側に足しても、残存率は 93.4% です。書き換えてから移した行は数えられていませんが、前回の 93.2% は空白や移動でほとんどずれていなかったと考えています。

git blame で消えた行の内訳 2026-07-30 に数えた 1,550 行 2 か月後に数えた 9,731 行 文面も残っていない 1,243 / 5,949 空行 235 / 2,705 記号だけの行 28 / 246 文面が残る文字のある行 44 / 831
緑の部分だけが、移動したのに消えたと数えた可能性のある行。数えたのは同じ 7 月のコミットで、測った時点だけが違う。

残存率を比べるなら、測るまでの期間を揃える

前回、新しい行は修正や削除の機会が少ないので高く出やすそうだ、と書きました。これも確かめるため、同じ 7 月のコミットを、この記事を書いた時点の最新コミット(2026-09-25 の 1961ecd)で数えました。

$ python3 survival.py a256f7f 1961ecd
2026-07-16  追加   5849  残存   2712   46.4%
2026-07-17  追加   1886  残存   1366   72.4%
2026-07-19  追加    236  残存    102   43.2%
2026-07-20  追加   3903  残存    248    6.4%
2026-07-21  追加   1176  残存   1074   91.3%
2026-07-22  追加    863  残存    692   80.2%
2026-07-24  追加    239  残存    151   63.2%
2026-07-25  追加   1103  残存   1019   92.4%
2026-07-26  追加   2851  残存   2393   83.9%
2026-07-27  追加   2370  残存   2159   91.1%
2026-07-29  追加   1638  残存    863   52.7%
2026-07-30  追加    720  残存    324   45.0%
合計  追加 22834  残存 13103  57.4%
消えた 9731 行のうち 文面も残っていない 5949、空行 2705、文字のある行 831、記号だけの行 246

93.2% だった残存率が 57.4% になりました。07-20 は 98.2% から 6.4% です。このリポジトリでは、8 月に英語版と日本語版の置き場を入れ替え、9 月には収録を減らして本文を書き直しました。

移動が増えたので、文面が残っている文字のある行も 831 行に増えました。それでも、全部を移動と見なして 61.0% です。消えた 9,731 行のうち 5,949 行は文面もどこにも残っていないので、大半は書き直しか削除で消えています。

書いた直後の数字と 2 か月後の数字は、同じ残存率として比べられません。比べるなら、書いてから測るまでの期間を揃える必要があります。

次はコードのリポジトリで試したい

今回は、ほとんどが日本語の文書のリポジトリでした。コードは、フォーマッターで空白が変わったり、関数を別のファイルへ移したりすることが多く、英数字も多いです。-w や -C で数字が変わるかもしれませんが、まだ試していません。

日本語の文書で、行の移動を追うほかの方法を知っていたら、X の @nnasaki で教えてください。