Skip to content

Instantly share code, notes, and snippets.

@alpaca-tc
Last active June 30, 2023 02:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alpaca-tc/649fd30b4f30400363fa56b62b8617b3 to your computer and use it in GitHub Desktop.
Save alpaca-tc/649fd30b4f30400363fa56b62b8617b3 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
# Usage:
# diffs = Dir["./patches/*.patch"].map { GitDiff.parse(File.read(_1)) }
# group_files = diffs.flat_map(&:files).group_by(&:old_path)
#
# group_files.each do |path, files|
# files = GitDiffMerger.new(files).compact_files
# git_diff = GitDiff.new(files:)
#
# File.write("#{__dir__}/patches/combined_patch_#{SecureRandom.hex}.patch", git_diff.to_s)
# end
class GitDiffMerger
# @param [Array<GitDiff::File>] files
def initialize(files)
@files = files
end
# 複数のdiffを可能な限り同一のファイルにまとめる
# 同一行に対するパッチの場合、2つ別れたままだと必ずコンフリクトするので、1つの変更にまとめたい
#
# @return [Array<GitDiff::File>]
def compact_files
same_files = @files.group_by { [_1.old_path, _1.new_path, _1.old_mode, _1.new_mode] }.values
same_files.flat_map do |files|
text_fragments = files.flat_map(&:text_fragments).flat_map(&:by_lines).sort_by { [_1.old_position, _1.old_lines] }
same_text_fragments = text_fragments.group_by { [_1.old_position, _1.new_position, _1.old_lines, _1.new_lines] }.values
file = files[0].dup
file.text_fragments = same_text_fragments.flat_map { compact_text_fragments(_1) }
file
end
end
def compact_text_fragments(text_fragments)
# 変更前の文言は共通が前提
base_text_fragment = text_fragments[0]
_, deleted_text = base_text_fragment.added_deleted_text
# diffは :equal, :delete, :insert のいずれかと、文字列を持つ配列
# [[:equal, " create(:user_, "],
# [:delete, "item"],
# [:insert, "sheet"]]
text_fragment_diffs = text_fragments.map do |text_fragment|
added_text, = text_fragment.added_deleted_text
diffs = diff_match_patch.diff_main(deleted_text, added_text)
diff_match_patch.diff_cleanup_semantic(diffs)
diffs
end
new_added_text = combine_diffs(deleted_text, text_fragment_diffs.uniq)
new_lines = base_text_fragment.lines.select(&:deleted?)
new_lines += new_added_text.split("\n").map.with_index { GitDiff::Line.new(operator: "+", line: _1, index: _2) }
new_text_fragment = base_text_fragment.dup
new_text_fragment.lines = new_lines
new_text_fragment
end
private
def combine_diffs(original_text, diffs)
# どの位置に文字が挿入されるかを記録する
inserted_map = Array.new(original_text.length)
# どの位置の文字列が削除対象かを記録する
# 本物のbitmapにしたかったが、楽なので配列でやる
deleted_bitmap = Array.new(original_text.length)
diffs.each do |diff|
pos = 0
diff.each do |(operator, string)|
case operator
when :equal
pos += string.length
when :delete
marked = Array.new(string.length, true)
raise "conflicted" if deleted_bitmap[pos, string.length].any? && deleted_bitmap[pos, string.length] != marked
deleted_bitmap[pos, string.length] = marked
pos += string.length
when :insert
if inserted_map[pos]
overlap_string = diff_overlap(inserted_map[pos], string)
raise "conflicted" unless overlap_string
inserted_map[pos] = overlap_string
else
inserted_map[pos] = string
end
else
raise "unknown operator: #{operator}"
end
end
end
buf = +""
original_text.length.times do |i|
if inserted_map[i]
buf << inserted_map[i]
end
unless deleted_bitmap[i]
buf << original_text[i]
end
end
buf
end
# @return [String, nil]
def diff_overlap(string_1, string_2)
if string_1.start_with?(string_2) || string_1.end_with?(string_2)
string_1
elsif string_2.start_with?(string_1) || string_2.end_with?(string_1)
string_2
end
end
def diff_match_patch
@diff_match_patch ||= DiffMatchPatch.new
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment