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?

共有日記の自動マージを 50.9% → 68.8% にした:git merge-file と jsdiff は得意な衝突が違う

0
Last updated at Posted at 2026-09-23

二人で同じ日の日記を書き換えたら、どうマージするか。

Whatcha は、夫婦や家族で一冊の日記を書くアプリです。同じ日のページを二人がそれぞれ編集することがあり、そのたびに「どちらの書き換えを残すか」を決める必要があります。

最初は JavaScript の diff ライブラリ(jsdiff)で 3-way マージをしていましたが、「自動でマージできませんでした」が多すぎました。git merge-file に替えて、それでも足りない部分を jsdiff に戻す二段構えにしたところ、ランダムに作った 4,000 通りの編集で、自動解決できる割合が 50.9% から 68.8% に上がりました。

この記事は、その計測と、二つの道具が「どこで」食い違うかの話です。

前提:日記を commit の列として持つ

1件の日記(entry)は、保存のたびに commit を1つ積みます。commit は差分ではなくその時点の全体のスナップショットで、親 commit を指します。

ブランチは日記1件ごとに持ち、先頭の commit を指します。ここで効いたのが 複合外部キーです。commit 表に (entry_id, id) の一意制約を置き、ブランチ側から (entry_id, head_commit_id) で参照するようにすると、別の日記の commit をブランチの先頭に置くことが DB の制約で不可能になります。アプリ側のチェックに頼らずに済みます。

書き込みは楽観ロックです。クライアントは「自分が見ていた head」を送り、サーバー側の head と違えば書き込まずに返します。git push が non-fast-forward で拒否されるのと同じ振る舞いです。

何をマージするか:項目ごとに規則が違う

日記1件には、本文のほかに「タイトル・気分・場所・時刻・表紙」といった単一の値と、「タグ」があります。これらは同じ規則ではマージしません。

単一の値:動いた側が勝つ

規則は古典的な 3-way そのままで、4行で書けます。

  1. 片方が base から動かしていなければ、もう片方の値を採る
  2. 逆も同じ
  3. 両方が同じ値に変えたなら、衝突ではない(これが地味に効きます)
  4. 別々の値に変えたときだけ衝突

「両方が同じ値に変えた」を衝突にしないだけで、実際の衝突はかなり減ります。二人で同じ場所に行って、二人とも場所欄に同じ地名を入れる、はよくあります。

タグ:集合として足し引きする。衝突させない

タグは集合として扱います。base の並びを保ったまま、どちらかが消したものを落とし、それぞれが足したものを後ろに足す。この規則だと衝突が定義上発生しません。

二人がそれぞれタグを1つずつ足す、は一番よくある操作です。これで「衝突しました」と言われたら使い物になりません。

本文:ここが本題

本文は複数行のテキストなので、行単位の 3-way マージ(diff3)になります。

計測:git と jsdiff は、別々のところで失敗する

ランダムに作った 4,000 通りの「base と二人の編集」に対して、両方の道具で自動マージを試しました。

結果 割合
両方とも解決できた 34.3%
git だけ解決できた 17.9%
jsdiff だけ解決できた 16.6%
どちらも解決できなかった 30.9%

どちらか一方だけを使うと:

方式 自動解決
jsdiff のみ(以前) 50.9%
git のみ 52.2%
git → だめなら jsdiff 68.8%

片方がもう片方の上位互換ではありません。 失敗する場所が違うので、重ねると伸びます。

git が衝突にするのは「隣り合った行」

git の 3-way マージは、変更箇所の前後の文脈を含めてひとかたまりとして扱います。なので、二人が隣の行をそれぞれ直すと、重なっていなくても衝突になります。

--diff-algorithm を myers / minimal / patience / histogram のどれに変えても同じだったので、設定の問題ではなく git の性質です。

コードなら妥当な判断ですが、共有日記では「同じ段落を二人で少しずつ直す」がむしろ普通です。ここを拾えるのが jsdiff でした。

jsdiff は「書いた行が消える」ことがある

一方で jsdiff には、マージ結果からどちらかが足した行が落ちるケースがありました。日記で他人の書いた文章が黙って消えるのは、衝突よりずっと悪い失敗です。

そこで jsdiff の結果には、事後条件の検査をかけています。

検査の内容は一文で書けます。「各側が base に対して足した行は、マージ結果に同じ回数以上残っていなければならない」。残っていなければ、そのマージは採用せず衝突として返します。

これはヒューリスティックではなく事後条件です。diff3 が内部で何をしていようと、健全な 3-way の結果は、両側が足した行を含んでいるはずだからです。比較は行の**重複度(多重集合)**で行います。同じ行をもう1回足した場合も「1行足した」と数える必要があるためです。

git の経路はこの失敗を起こさないので、検査は jsdiff 側にだけかけています。

git merge-file は「リポジトリの要らない純粋関数」

git merge-file は、ファイルを3つ受け取ってマージ結果を返すだけのコマンドです。リポジトリも index も作業ツリーも要らず、状態を残しません。欲しいのは git のアルゴリズムであって、git をもう一つのデータ置き場にすることではないので、これがちょうど合います。

呼び出しは、一時ディレクトリに3つのファイルを書いて git merge-file -p ours base theirs を実行し、標準出力を受け取るだけです。気をつけた点:

  • 終了コードがそのまま衝突数(1〜127、255 は git 自体の失敗)。0 以外なら衝突として扱う
  • git が入っていないのを「衝突」と扱わない。 そうすると、イメージの作り間違い1つで全マージが黙って衝突になる。例外にして落とす(API の Dockerfile に git を入れている)
  • stderr を捨てる。 衝突は正常な結果なのに、git は非ゼロ終了を stderr に書くので、二人が同じ段落を直すたびにログに fatal: が並ぶ

ほかにハマったところ

  • 改行コード。 片方のクライアントが CRLF で送ると、jsdiff には「全行を書き換えた」ように見えて、その(実質空の)変更が本文全体に勝ち、相手の文章の改行コードまで黙って変わる。マージ中は LF にそろえ、結果はマージ先のブランチの改行コードに戻す
  • base が空。 新しく作ったばかりの日記は本文が "" で、共通の祖先が無い。二人が別の文章を足したら、それは本物の add/add 衝突として返す(jsdiff は空の base をそもそも受け付けない)
  • 完全に同じなら何もしない。 先に threeWayPick をバイト単位で試し、マージが要らない本文は保存されたとおりに返す

まとめ

  • テキストの 3-way マージは、道具によって失敗する場所が違う。一つに決めず、重ねる
  • 速さや賢さより、誰かが書いた文章を消さないことを事後条件で保証する
  • git は「アルゴリズムだけ借りる」使い方ができる。git merge-file はそのための入口


最後に、お願いがあります。

この記事のマージを実際に動かしているのが Whatcha です。一度だけ、ブラウザで開いて一行書いてみてもらえませんか。 登録もメールアドレスも要りません。30秒で終わります。

二人で同じ日のページを書くとどうなるかは、コードの説明を読むより、実際に触ったほうが早いです。相手がいなくても、自分ひとりで開いて書いてみるだけで構いません。触ってみて「ここが変」と思ったら、そのまま教えてください。 それが次に直すところになります。

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?