公開用のリポジトリにJavaScriptのファイルを1つ足したら、GitHubが差分を出さなくなりました。 画面に出るのはこの1行だけです。
static/app.js | Bin 138544 -> 142619 bytes
Bin ——バイナリ扱いです。テキストのはずのJavaScriptが、
画像や実行ファイルと同じ扱いになっている。差分が読めないので、レビューもできません。
最初は文字コードか改行コードを疑いました。どちらも違いました。 原因が分かるまでの手順と、実際の原因を書きます。
まず、そのファイルが本当にテキストとして読めるのかを見ます。
file static/app.js
返ってきたのは data でした。同じ種類の別ファイルは UTF-8 Unicode text と出ます。
つまりgitの気まぐれではなく、ファイルの中身がテキストとして扱えない状態だと分かります。
ここで文字コードの破損を疑いましたが、Pythonで decode('utf-8') は通りました。
UTF-8としては正しい。壊れているわけではありません。
gitは、ファイルの先頭のかたまりを見てNULバイト(0x00)が含まれていればバイナリと判定します。
拡張子は見ていません。.js でも .txt でも、NULが1つ入っていればバイナリです。
つまり探すべきものは「文字化け」ではなく、本来テキストに入らないはずの制御文字でした。
ファイルを1バイトずつ見て、制御文字の位置を出します。
bad = [(i, c) for i, c in enumerate(open('app.js','rb').read()) if c < 9 or 13 < c < 32]
結果は2バイトだけ。142,619バイト中の2バイトです。しかも隣り合っていました。
0x00 と 0x1F。前後の文字列を出すと、こう書かれていました。
if (p.length > 3 && !/[■-■]/.test(p) && ...
※ ■ の位置に入っていたのが問題のバイト。画面上は何も見えません(幅ゼロで表示されるため、 ここでは便宜的に ■ と書いています)
正規表現です。「制御文字を除外する」という意図のコードに、制御文字が生で入っていた。 そして見えないので、コードを目で追っても気づけません。
本来は /[\x00-\x1f]/ と4文字の文字列として書くべきところが、
\x00 と \x1f が実際のバイトそのものとして保存されていました。
正規表現としては同じ意味なので、コードは正しく動きます。エラーも出ません。
動くのに、ファイルがバイナリになる——これが厄介なところでした。
生のバイトを、エスケープした文字列に置き換えます。意味は変わりません。
置換したら file の結果は UTF-8 Unicode text に戻り、
差分も Bin ではなく 104 insertions(+), 17 deletions(-) と、
ふつうの行単位の表示になりました。
はっきりした原因は、エディタやツールを経由するあいだに エスケープ表記が「解釈されて」実体のバイトになったことだと考えています。 たとえば文字列としてこの正規表現を組み立てて、どこかで一度展開してからファイルに書けば、こうなります。
実際、この記事を書くために調査していたときも、シェル経由でバックスラッシュを渡そうとして
同じ取り違えを起こしました。\\x00 と書いたつもりが \x00 になって届き、
「置換したのに何も変わらない」という状態になった。バックスラッシュは、経路の数だけ消えます。
file で確かめる——data と出たら中身の問題。gitの設定ではない0x00 が1つでもあればgitはバイナリと判定するfile と git diff --stat の両方で戻ったことを確認するちなみに .gitattributes に *.js text と書けば、
gitに「これはテキストだ」と強制することもできます。
ただしそれは表示を変えるだけで、混入した制御文字は残ります。
症状を隠すことになるので、まず中身を直したほうがいいと思います。
file → UTF-8復号 → 制御文字の位置、の順で1分で特定できる※Windows 11・Git for Windows の環境で実際に踏んだものです。バイト数や差分の行数は実測値です。