0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

たった1行の変更で数百行のdiffが出た話 – 改行コードとscp運用の落とし穴

0
Posted at

環境情報

  • リモート: Linuxサーバー (Ubuntu 22.04 LTS) / ファイルはLF改行
  • ローカル: Windows 10 Pro + Git for Windows 2.39.0 / リポジトリはCRLF改行
  • 取得・反映手段: scp (OpenSSH 8.9p1)
  • 差分確認: git diff --stat / git diff --ignore-space-at-eol

はじめに

リモートLinuxサーバー上のコードをscpでローカルに取得し、編集後にscpで書き戻す、という運用をしているチームは多いです。私もその一人で、ある日「実質1行しか変更していないのに、git管理ミラーに反映しようとしたら数百行のdiffが発生する」という現象に遭遇しました。

原因は「改行コードの違い」でした。リモートはLF、ローカルgit管理下はCRLF。ファイル全体をcpやscpで上書きすると、gitが管理しているファイルの改行コードまで丸ごと置き換わり、無駄な差分が生まれるのです。本記事では、この問題の再現手順と、perl -i -pe を使った最小差分での解決方法を詳しく解説します。

何が起きたのか

具体的な運用フローは以下の通りです。

  1. リモートサーバーの /var/www/app/config.ini をscpで取得する
  2. ローカルで「設定値の変更」を1行だけ行う
  3. 編集後、scpで同一パスに書き戻す
  4. リポジトリ(ローカルのgit管理下)にコピーして git diff を確認

このとき、変更したのは MAX_RETRY = 3MAX_RETRY = 5 に書き換えた1行だけ。しかし git diff --stat の結果はこうでした。

 config.ini | 128 ++++++++++++++++++++++++++++++++++++------------------------
 1 file changed, 64 insertions(+), 64 deletions(-)

128行のうち64行が削除・64行が追加された扱いです。実際には1行しか変わっていないのに、です。これはgitが改行コードの違いを「全行の削除+追加」として認識したためです。

原因:改行コードの違い

改行コードには主に以下の3種類があります。

改行コード 主なOS
LF \n (0x0A) Linux / macOS
CRLF \r\n (0x0D 0x0A) Windows
CR \r (0x0D) 古典的Mac

今回の場合、リモートサーバー上のファイルはLFでした。一方、ローカルのgit管理下では core.autocrlf=true が設定されており、チェックアウト時にCRLFへ変換されていました。つまりローカルにあるファイルは「git管理上はLFだが、作業ツリー上はCRLF」という状態です。

このファイルをscpでリモートに書き戻し、さらにミラーリポジトリへコピーすると、CRLFのままgitの管理下に置かれます。gitは内容比較時に改行コードを正規化しますが、通常の git diff ではすべての行が「改行コード違い」として差分扱いになるのです。

検証: 最小再現

実際に確認してみましょう。まずリモート側のファイルを取得します。

# リモートで LF のファイルを確認
remote$ file config.ini
config.ini: ASCII text

remote$ cat -A config.ini | head -3
MAX_RETRY = 3$
TIMEOUT = 30$
LOG_LEVEL = info$

$ は行末のLFを示しています。これをローカルにscpで取得します。

# ローカルに取得
local$ scp user@remote:/var/www/app/config.ini .

# 改行コードを確認
local$ file config.ini
config.ini: ASCII text, with CRLF line terminators

scp自体は改行コードを変換しません。しかし、ローカルリポジトリ内ではgitの core.autocrlf 設定により、取得前にCRLFになっていた可能性があります。さらに、ローカルで編集した後にscpで書き戻すと、CRLFのままリモートに送られます。

# ローカルで編集して書き戻し
local$ perl -pi -e 's/MAX_RETRY = 3/MAX_RETRY = 5/' config.ini
local$ scp config.ini user@remote:/var/www/app/config.ini

そしてミラーリポジトリ(ローカルのgit管理下)にコピーしたとき、git diff が大量差分になります。

local$ cp config.ini /path/to/git_mirror/config.ini
local$ cd /path/to/git_mirror
local$ git diff --stat
config.ini | 128 ++++++++++++++++++++++++++++++++----------------
 1 file changed, 64 insertions(+), 64 deletions(-)

git diff --ignore-space-at-eol を付けると、実際には1行しか変わっていないことが確認できます。

local$ git diff --ignore-space-at-eol
 config.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

これが「1行しか変えていないのに数百行のdiff」の正体です。

解決策: perl -i -pe による部分置換

大量差分を防ぐには、ファイル全体を上書きせず、対象の行だけをテキスト置換する方法が有効です。私は以下のperlワンライナーを使いました。

local$ perl -i -pe 's/MAX_RETRY = 3/MAX_RETRY = 5/' config.ini

このコマンドの動作を分解すると次の通りです。

  • -p: 入力を1行ずつ読んで処理し、printする
  • -i: ファイルを上書きする(バックアップなし)
  • -e: 後続のコードを実行する

-p は行単位で処理するため、既存のファイルの改行コード(CRLFかLFか)を維持したまま、該当行の内容だけを置換します。そのため、gitが管理しているファイルの改行コードを壊さずに済みます。

実際に実行した後の差分は最小限になりました。

local$ git diff --stat
 config.ini | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

scpで書き戻す際も、リモート側のファイルはLFのままですから、他のエンジニアが編集しても余計な差分は発生しません。

比較: 他の解決策

今回の問題に対する方法をいくつか比較しました。

手法 改行コード保持 差分最小 注意点
ファイル全体をcp/scpで上書き × × 一番シンプルだが今回の原因
dos2unix / unix2dos で変換 変換コマンドが入っている必要がある
sed -i GNU sed依存、環境差あり
perl -i -pe ほぼどの環境にもあるperlが必要
gitの core.autocrlf 設定変更 過去の履歴には影響しない

dos2unix は確実に改行コードを正規化できますが、リポジトリ内のファイル全部に対して行うと大量差分が再び発生します。今回のケースでは「置換したい行だけを対象にする」ことが重要なので、perlの部分置換が最も適していました。

運用で気をつけるべきポイント

今回の騒動で学んだことをまとめます。

  • scpやcpによる全量上書きは、改行コードを含む「ファイルの状態」をそのまま持ち込む
  • git管理下のファイルを外部サーバーと往復させるときは、必ず file コマンドで改行コードを確認する
  • 変更をミラーへ反映するときは、ファイルコピーではなく git apply やパッチ適用を検討する
  • チームで運用ルールを決める: 「リモート側は常にLF」「ローカル側もLFに統一する」「gitの core.autocrlf 設定を明示的に共有する」
  • 置換系の作業には perl -i -pe を利用し、不用意な改行コード変換を防ぐ

また、もし既にCRLFで汚染されてしまった場合は、以下のコマンドでLFに戻してからコミットし直すのも手です。

# 改行コードをLFに統一
find . -type f -name '*.ini' -exec perl -pi -e 's/\r\n/\n/g' {} \;

ただし、この作業も再度大きな差分を生むため、作業前にブランチを切り、レビューで差分を確認することをおすすめします。

FAQ

Q. perl -i -pe はファイル全体をメモリに読み込むの?

A. いいえ。-p は1行ずつ読み込んで処理するため、大きなファイルでもメモリ消費は少ないです。ただし -i は一時ファイルを生成して入れ替える方式なので、ディスク容量には注意してください。

Q. バイナリファイルで使っても大丈夫?

A. 危険です。perlのテキストモードでは改行コードが変換される場合があります。バイナリを扱う場合は binmode を設定してください。

perl -i -pe 'BEGIN{binmode STDIN; binmode STDOUT} s/.../.../' binary_file

Q. sed -i ではダメなの?

A. 使えます。ただしGNU sedとBSD sedでオプションが微妙に異なり、sed -i ''sed -i の違いで事故が起きやすいです。perlはほぼ全環境で同じ書き方が通用するため、私はperlを選びました。

Q. そもそもscp運用をやめたいのですが

A. おすすめです。rsyncやGitリポジトリの直接pull/pushに移行すると、改行コード問題の多くがgitの正規化機能で吸収されます。ただし、リモートが本番サーバーでgitインストール不可の場合は、今回の「部分置換で書き戻す」運用が現実的です。

まとめ

今回は「scpで取得したファイルを1行だけ編集して書き戻したら、git差分が数百行になった」問題を解決しました。

  • 原因は改行コードの違い(リモートLF / ローカルCRLF)
  • 解決策は perl -i -pe による行単位の部分置換
  • ファイル全体を上書きしないことで、既存の改行コードを保持できる
  • 文末にある「みなさんは改行コードの管理、どうしてますか?」への答えとして、私は「gitの設定と運用ルールを組み合わせて防ぐ」という結論に至りました

ファイル編集の基本中の基本ですが、scpとgitを組み合わせたときに見落としがちな罠です。同じような運用をしているエンジニアの参考になれば幸いです。


この記事を書いた人

BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。

GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。

👉 BENTEN Web Works — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?