Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

unified_diff.rb 8.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2020 Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. module Redmine
  19. # Class used to parse unified diffs
  20. class UnifiedDiff < Array
  21. attr_reader :diff_type, :diff_style
  22. def initialize(diff, options={})
  23. options.assert_valid_keys(:type, :style, :max_lines)
  24. diff = diff.split("\n") if diff.is_a?(String)
  25. @diff_type = options[:type] || 'inline'
  26. @diff_style = options[:style]
  27. # remove git footer
  28. if diff.length > 1 &&
  29. diff[-2] =~ /^--/ &&
  30. diff[-1] =~ /^[0-9]/
  31. diff.pop(2)
  32. end
  33. lines = 0
  34. @truncated = false
  35. diff_table = DiffTable.new(diff_type, diff_style)
  36. diff.each do |line_raw|
  37. line = Redmine::CodesetUtil.to_utf8_by_setting(line_raw)
  38. unless diff_table.add_line(line)
  39. self << diff_table if diff_table.length > 0
  40. diff_table = DiffTable.new(diff_type, diff_style)
  41. end
  42. lines += 1
  43. if options[:max_lines] && lines > options[:max_lines]
  44. @truncated = true
  45. break
  46. end
  47. end
  48. self << diff_table unless diff_table.empty?
  49. self
  50. end
  51. def truncated?; @truncated; end
  52. end
  53. # Class that represents a file diff
  54. class DiffTable < Array
  55. attr_reader :file_name, :previous_file_name
  56. # Initialize with a Diff file and the type of Diff View
  57. # The type view must be inline or sbs (side_by_side)
  58. def initialize(type="inline", style=nil)
  59. @parsing = false
  60. @added = 0
  61. @removed = 0
  62. @type = type
  63. @style = style
  64. @file_name = nil
  65. @previous_file_name = nil
  66. @git_diff = false
  67. end
  68. # Function for add a line of this Diff
  69. # Returns false when the diff ends
  70. def add_line(line)
  71. unless @parsing
  72. if line =~ /^(---|\+\+\+) (.*)$/
  73. self.file_name = $2
  74. elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
  75. @line_num_l = $2.to_i
  76. @line_num_r = $5.to_i
  77. @parsing = true
  78. end
  79. else
  80. if %r{^[^\+\-\s@\\]}.match?(line)
  81. @parsing = false
  82. return false
  83. elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
  84. @line_num_l = $2.to_i
  85. @line_num_r = $5.to_i
  86. else
  87. parse_line(line, @type)
  88. end
  89. end
  90. return true
  91. end
  92. def each_line
  93. prev_line_left, prev_line_right = nil, nil
  94. each do |line|
  95. spacing = prev_line_left && prev_line_right && (line.nb_line_left != prev_line_left+1) && (line.nb_line_right != prev_line_right+1)
  96. yield spacing, line
  97. prev_line_left = line.nb_line_left.to_i if line.nb_line_left.to_i > 0
  98. prev_line_right = line.nb_line_right.to_i if line.nb_line_right.to_i > 0
  99. end
  100. end
  101. def inspect
  102. puts '### DIFF TABLE ###'
  103. puts "file : #{file_name}"
  104. self.each do |d|
  105. d.inspect
  106. end
  107. end
  108. private
  109. def file_name=(arg)
  110. both_git_diff = false
  111. if file_name.nil?
  112. @git_diff = true if %r{^(a/|/dev/null)}.match?(arg)
  113. else
  114. both_git_diff = (@git_diff && %r{^(b/|/dev/null)}.match?(arg))
  115. end
  116. if both_git_diff
  117. if file_name && arg == "/dev/null"
  118. # keep the original file name
  119. @file_name = file_name.sub(%r{^a/}, '')
  120. else
  121. # remove leading a/
  122. @previous_file_name = file_name.sub(%r{^a/}, '') unless file_name == "/dev/null"
  123. # remove leading b/
  124. @file_name = arg.sub(%r{^b/}, '')
  125. @previous_file_name = nil if @previous_file_name == @file_name
  126. end
  127. elsif @style == "Subversion"
  128. # removing trailing "(revision nn)"
  129. @file_name = arg.sub(%r{\t+\(.*\)$}, '')
  130. else
  131. @file_name = arg
  132. end
  133. end
  134. def diff_for_added_line
  135. if @type == 'sbs' && @removed > 0 && @added < @removed
  136. self[-(@removed - @added)]
  137. else
  138. diff = Diff.new
  139. self << diff
  140. diff
  141. end
  142. end
  143. def parse_line(line, type="inline")
  144. if line[0, 1] == "+"
  145. diff = diff_for_added_line
  146. diff.line_right = line[1..-1]
  147. diff.nb_line_right = @line_num_r
  148. diff.type_diff_right = 'diff_in'
  149. @line_num_r += 1
  150. @added += 1
  151. true
  152. elsif line[0, 1] == "-"
  153. diff = Diff.new
  154. diff.line_left = line[1..-1]
  155. diff.nb_line_left = @line_num_l
  156. diff.type_diff_left = 'diff_out'
  157. self << diff
  158. @line_num_l += 1
  159. @removed += 1
  160. true
  161. else
  162. write_offsets
  163. if /\s/.match?(line[0, 1])
  164. diff = Diff.new
  165. diff.line_right = line[1..-1]
  166. diff.nb_line_right = @line_num_r
  167. diff.line_left = line[1..-1]
  168. diff.nb_line_left = @line_num_l
  169. self << diff
  170. @line_num_l += 1
  171. @line_num_r += 1
  172. true
  173. elsif line[0, 1] = "\\"
  174. true
  175. else
  176. false
  177. end
  178. end
  179. end
  180. def write_offsets
  181. if @added > 0 && @added == @removed
  182. @added.times do |i|
  183. line = self[-(1 + i)]
  184. removed = (@type == 'sbs') ? line : self[-(1 + @added + i)]
  185. offsets = offsets(removed.line_left, line.line_right)
  186. removed.offsets = line.offsets = offsets
  187. end
  188. end
  189. @added = 0
  190. @removed = 0
  191. end
  192. def offsets(line_left, line_right)
  193. if line_left.present? && line_right.present? && line_left != line_right
  194. max = [line_left.size, line_right.size].min
  195. starting = 0
  196. while starting < max &&
  197. line_left[starting] == line_right[starting]
  198. starting += 1
  199. end
  200. ending = -1
  201. while ending >= -(max - starting) &&
  202. (line_left[ending] == line_right[ending])
  203. ending -= 1
  204. end
  205. unless starting == 0 && ending == -1
  206. [starting, ending]
  207. end
  208. end
  209. end
  210. end
  211. # A line of diff
  212. class Diff
  213. attr_accessor :nb_line_left
  214. attr_accessor :line_left
  215. attr_accessor :nb_line_right
  216. attr_accessor :line_right
  217. attr_accessor :type_diff_right
  218. attr_accessor :type_diff_left
  219. attr_accessor :offsets
  220. def initialize
  221. self.nb_line_left = ''
  222. self.nb_line_right = ''
  223. self.line_left = ''
  224. self.line_right = ''
  225. self.type_diff_right = ''
  226. self.type_diff_left = ''
  227. end
  228. def type_diff
  229. type_diff_right == 'diff_in' ? type_diff_right : type_diff_left
  230. end
  231. def line
  232. type_diff_right == 'diff_in' ? line_right : line_left
  233. end
  234. def html_line_left
  235. line_to_html(line_left, offsets)
  236. end
  237. def html_line_right
  238. line_to_html(line_right, offsets)
  239. end
  240. def html_line
  241. line_to_html(line, offsets)
  242. end
  243. def inspect
  244. puts '### Start Line Diff ###'
  245. puts self.nb_line_left
  246. puts self.line_left
  247. puts self.nb_line_right
  248. puts self.line_right
  249. end
  250. private
  251. def line_to_html(line, offsets)
  252. html = line_to_html_raw(line, offsets)
  253. html.force_encoding('UTF-8')
  254. html
  255. end
  256. def line_to_html_raw(line, offsets)
  257. if offsets
  258. s = +''
  259. unless offsets.first == 0
  260. s << CGI.escapeHTML(line[0..offsets.first-1])
  261. end
  262. s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>'
  263. unless offsets.last == -1
  264. s << CGI.escapeHTML(line[offsets.last+1..-1])
  265. end
  266. s
  267. else
  268. CGI.escapeHTML(line)
  269. end
  270. end
  271. end
  272. end