-
-
Save alpaca-tc/649fd30b4f30400363fa56b62b8617b3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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