-
-
Save alpaca-tc/412ec9d2c10044bd4500eadbe40459de 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)) } | |
class GitDiff | |
class File | |
include ActiveModel::Model | |
include ActiveModel::Attributes | |
attribute :old_path, :string | |
attribute :new_path, :string | |
attribute :old_index, :string | |
attribute :new_index, :string | |
attribute :old_mode, :string | |
attribute :new_mode, :string | |
attribute :text_fragments, default: -> { [] } | |
validates :old_path, presence: true | |
validates :new_path, presence: true | |
validates :old_index, presence: true | |
validates :new_index, presence: true | |
validates :old_mode, presence: true | |
validates :new_mode, presence: true | |
validate do | |
errors.add(:text_fragments) unless text_fragments.map(&:valid?).all? | |
end | |
end | |
class TextFragment | |
include ActiveModel::Model | |
include ActiveModel::Attributes | |
attribute :comment, :string | |
attribute :old_position, :integer | |
attribute :old_lines, :integer | |
attribute :new_position, :integer | |
attribute :new_lines, :integer | |
attribute :lines, default: -> { [] } | |
validates :old_position, presence: true | |
validates :old_lines, presence: true | |
validates :new_position, presence: true | |
validates :new_lines, presence: true | |
validate do | |
errors.add(:lines) unless lines.map(&:valid?).all? | |
end | |
# @return [Array<GitDiff::TextFragment>] | |
def by_lines | |
if old_lines == 0 && new_lines == 0 | |
[self] | |
else | |
lines.group_by(&:index).map do |index, index_lines| | |
self.class.new( | |
comment:, | |
old_position: old_position + index, | |
old_lines: 0, | |
new_position: new_position + index, | |
new_lines: 0, | |
lines: index_lines, | |
) | |
end | |
end | |
end | |
# @param line [GitDiff::Line] | |
def add_line(line) | |
index = lines.count { _1.operator == line.operator } | |
line.index = index | |
lines << line | |
end | |
# @return [Array<Array<GitDiff::Line>, Array<GitDiff::Line>>] [added, deleted]の配列 | |
def added_deleted | |
lines.partition { _1.operator == "+" }.map { _1.sort_by(&:index) } | |
end | |
def added_deleted_text | |
added, deleted = added_deleted | |
added_text = added.map(&:line).join("\n") | |
deleted_text = deleted.map(&:line).join("\n") | |
[added_text, deleted_text] | |
end | |
def to_s | |
buf = +"" | |
buf << "@@ -#{old_position}" | |
buf << ",#{old_lines}" if old_lines > 0 | |
buf << " " | |
buf << "+#{new_position}" | |
buf << ",#{new_lines}" if new_lines > 0 | |
buf << " @@" | |
buf << comment | |
buf << "\n" | |
buf << lines.map(&:to_s).join("\n") | |
end | |
alias_method :inspect, :to_s | |
end | |
class Line | |
include ActiveModel::Model | |
include ActiveModel::Attributes | |
attribute :operator, :string | |
attribute :line, :string | |
attribute :index, :integer | |
validates :operator, inclusion: { in: %w(+ -) } | |
validates :line, presence: true | |
validates :index, presence: true | |
def deleted? | |
operator == "-" | |
end | |
def to_s | |
"#{operator}#{line}" | |
end | |
alias_method :inspect, :to_s | |
end | |
class Parser | |
DIFF_GIT_RE = %r{^diff --git (?:a|i|w|c|o|1|2)/(?<old_path>.*) (?:b|i|w|c|o|1|2)/(?<new_path>.*)$} | |
DIFF_INDEX_RE = /^index (?<old_index>[0-9a-f]+)\.\.(?<new_index>[0-9a-f]+)(?: (?<new_mode>\d+))?$/ | |
DIFF_OLD_MODE_RE = /^old mode (?<old_mode>\d+)$/ | |
DIFF_NEW_MODE_RE = /^new mode (?<new_mode>\d+)$/ | |
DIFF_OLD_RE = %r{^--- (?:a|i|w|c|o|1|2)/.*$} | |
DIFF_NEW_RE = %r{^\+\+\+ (?:b|i|w|c|o|1|2)/.*$} | |
DIFF_TO_RE = /---/ | |
TEXT_FLAGMENT_BEG_RE = /^@@ -(?<old_position>\d+)(?:,(?<old_lines>\d+))? \+(?<new_position>\d+)(?:,(?<new_lines>\d+))? @@(?<comment>.*)$/ | |
TEXT_FLAGMENT_RE = /^(?<operator>\+|-)(?<line>.*)$/ | |
EOF_RE = /^\\ No newline at end of file$/ | |
class OutsideTextFragmentError < StandardError; end | |
def initialize(content) | |
@content = content | |
end | |
def to_files | |
files = [] | |
file = nil | |
within_text_fragment = false | |
text_fragment = nil | |
@content.lines.each_with_index do |line, _count| | |
case line | |
when DIFF_GIT_RE | |
within_text_fragment = false | |
text_fragment = nil | |
file&.validate! | |
file = GitDiff::File.new | |
files << file | |
file.old_path = Regexp.last_match[:old_path] | |
file.new_path = Regexp.last_match[:new_path] | |
when DIFF_OLD_MODE_RE | |
file.old_mode = Regexp.last_match[:old_mode] | |
when DIFF_NEW_MODE_RE | |
file.new_mode = Regexp.last_match[:new_mode] | |
when DIFF_INDEX_RE | |
file.old_index = Regexp.last_match[:old_index] | |
file.new_index = Regexp.last_match[:new_index] | |
file.new_mode = Regexp.last_match[:new_mode] | |
when DIFF_OLD_RE | |
# skip | |
when DIFF_NEW_RE | |
within_text_fragment = true | |
when TEXT_FLAGMENT_BEG_RE | |
text_fragment = GitDiff::TextFragment.new( | |
comment: Regexp.last_match[:comment], | |
old_position: Regexp.last_match[:old_position].to_i, | |
old_lines: Regexp.last_match[:old_lines].to_i, | |
new_position: Regexp.last_match[:new_position].to_i, | |
new_lines: Regexp.last_match[:new_lines].to_i, | |
) | |
file.text_fragments << text_fragment | |
when TEXT_FLAGMENT_RE | |
raise OutsideTextFragmentError unless within_text_fragment | |
text_fragment.add_line(GitDiff::Line.new(operator: Regexp.last_match[:operator], line: Regexp.last_match[:line])) | |
when EOF_RE | |
# skip | |
else | |
raise "unknown line: #{line}" | |
end | |
end | |
files | |
end | |
end | |
attr_reader :files | |
def self.parse(content) | |
files = Parser.new(content).to_files | |
new(files:) | |
end | |
# @param files [GitDiff::File] | |
def initialize(files: []) | |
raise ArgumentError, "files is blank" if files.empty? | |
@files = files | |
end | |
def to_s | |
buf = +"" | |
files.each do |file| | |
buf << "diff --git a/#{file.old_path} b/#{file.new_path}\n" | |
# NOTE: changeだけ対応している | |
buf << "index #{file.old_index}..#{file.new_index} #{file.new_mode}\n" | |
buf << "--- a/#{file.old_path}\n" | |
buf << "+++ b/#{file.new_path}\n" | |
buf << file.text_fragments.map(&:to_s).join("\n") | |
buf << "\n" | |
end | |
buf | |
end | |
alias_method :inspect, :to_s | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment