Show More
@@ -0,0 +1,14 b'' | |||
|
1 | Copyright 2006 Google Inc. | |
|
2 | http://code.google.com/p/google-diff-match-patch/ | |
|
3 | ||
|
4 | Licensed under the Apache License, Version 2.0 (the "License"); | |
|
5 | you may not use this file except in compliance with the License. | |
|
6 | You may obtain a copy of the License at | |
|
7 | ||
|
8 | http://www.apache.org/licenses/LICENSE-2.0 | |
|
9 | ||
|
10 | Unless required by applicable law or agreed to in writing, software | |
|
11 | distributed under the License is distributed on an "AS IS" BASIS, | |
|
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
|
13 | See the License for the specific language governing permissions and | |
|
14 | limitations under the License. |
This diff has been collapsed as it changes many lines, (1919 lines changed) Show them Hide them | |||
@@ -0,0 +1,1919 b'' | |||
|
1 | #!/usr/bin/python2.4 | |
|
2 | ||
|
3 | from __future__ import division | |
|
4 | ||
|
5 | """Diff Match and Patch | |
|
6 | ||
|
7 | Copyright 2006 Google Inc. | |
|
8 | http://code.google.com/p/google-diff-match-patch/ | |
|
9 | ||
|
10 | Licensed under the Apache License, Version 2.0 (the "License"); | |
|
11 | you may not use this file except in compliance with the License. | |
|
12 | You may obtain a copy of the License at | |
|
13 | ||
|
14 | http://www.apache.org/licenses/LICENSE-2.0 | |
|
15 | ||
|
16 | Unless required by applicable law or agreed to in writing, software | |
|
17 | distributed under the License is distributed on an "AS IS" BASIS, | |
|
18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
|
19 | See the License for the specific language governing permissions and | |
|
20 | limitations under the License. | |
|
21 | """ | |
|
22 | ||
|
23 | """Functions for diff, match and patch. | |
|
24 | ||
|
25 | Computes the difference between two texts to create a patch. | |
|
26 | Applies the patch onto another text, allowing for errors. | |
|
27 | """ | |
|
28 | ||
|
29 | __author__ = 'fraser@google.com (Neil Fraser)' | |
|
30 | ||
|
31 | import math | |
|
32 | import re | |
|
33 | import sys | |
|
34 | import time | |
|
35 | import urllib | |
|
36 | ||
|
37 | class diff_match_patch: | |
|
38 | """Class containing the diff, match and patch methods. | |
|
39 | ||
|
40 | Also contains the behaviour settings. | |
|
41 | """ | |
|
42 | ||
|
43 | def __init__(self): | |
|
44 | """Inits a diff_match_patch object with default settings. | |
|
45 | Redefine these in your program to override the defaults. | |
|
46 | """ | |
|
47 | ||
|
48 | # Number of seconds to map a diff before giving up (0 for infinity). | |
|
49 | self.Diff_Timeout = 1.0 | |
|
50 | # Cost of an empty edit operation in terms of edit characters. | |
|
51 | self.Diff_EditCost = 4 | |
|
52 | # At what point is no match declared (0.0 = perfection, 1.0 = very loose). | |
|
53 | self.Match_Threshold = 0.5 | |
|
54 | # How far to search for a match (0 = exact location, 1000+ = broad match). | |
|
55 | # A match this many characters away from the expected location will add | |
|
56 | # 1.0 to the score (0.0 is a perfect match). | |
|
57 | self.Match_Distance = 1000 | |
|
58 | # When deleting a large block of text (over ~64 characters), how close do | |
|
59 | # the contents have to be to match the expected contents. (0.0 = perfection, | |
|
60 | # 1.0 = very loose). Note that Match_Threshold controls how closely the | |
|
61 | # end points of a delete need to match. | |
|
62 | self.Patch_DeleteThreshold = 0.5 | |
|
63 | # Chunk size for context length. | |
|
64 | self.Patch_Margin = 4 | |
|
65 | ||
|
66 | # The number of bits in an int. | |
|
67 | # Python has no maximum, thus to disable patch splitting set to 0. | |
|
68 | # However to avoid long patches in certain pathological cases, use 32. | |
|
69 | # Multiple short patches (using native ints) are much faster than long ones. | |
|
70 | self.Match_MaxBits = 32 | |
|
71 | ||
|
72 | # DIFF FUNCTIONS | |
|
73 | ||
|
74 | # The data structure representing a diff is an array of tuples: | |
|
75 | # [(DIFF_DELETE, "Hello"), (DIFF_INSERT, "Goodbye"), (DIFF_EQUAL, " world.")] | |
|
76 | # which means: delete "Hello", add "Goodbye" and keep " world." | |
|
77 | DIFF_DELETE = -1 | |
|
78 | DIFF_INSERT = 1 | |
|
79 | DIFF_EQUAL = 0 | |
|
80 | ||
|
81 | def diff_main(self, text1, text2, checklines=True, deadline=None): | |
|
82 | """Find the differences between two texts. Simplifies the problem by | |
|
83 | stripping any common prefix or suffix off the texts before diffing. | |
|
84 | ||
|
85 | Args: | |
|
86 | text1: Old string to be diffed. | |
|
87 | text2: New string to be diffed. | |
|
88 | checklines: Optional speedup flag. If present and false, then don't run | |
|
89 | a line-level diff first to identify the changed areas. | |
|
90 | Defaults to true, which does a faster, slightly less optimal diff. | |
|
91 | deadline: Optional time when the diff should be complete by. Used | |
|
92 | internally for recursive calls. Users should set DiffTimeout instead. | |
|
93 | ||
|
94 | Returns: | |
|
95 | Array of changes. | |
|
96 | """ | |
|
97 | # Set a deadline by which time the diff must be complete. | |
|
98 | if deadline == None: | |
|
99 | # Unlike in most languages, Python counts time in seconds. | |
|
100 | if self.Diff_Timeout <= 0: | |
|
101 | deadline = sys.maxint | |
|
102 | else: | |
|
103 | deadline = time.time() + self.Diff_Timeout | |
|
104 | ||
|
105 | # Check for null inputs. | |
|
106 | if text1 == None or text2 == None: | |
|
107 | raise ValueError("Null inputs. (diff_main)") | |
|
108 | ||
|
109 | # Check for equality (speedup). | |
|
110 | if text1 == text2: | |
|
111 | if text1: | |
|
112 | return [(self.DIFF_EQUAL, text1)] | |
|
113 | return [] | |
|
114 | ||
|
115 | # Trim off common prefix (speedup). | |
|
116 | commonlength = self.diff_commonPrefix(text1, text2) | |
|
117 | commonprefix = text1[:commonlength] | |
|
118 | text1 = text1[commonlength:] | |
|
119 | text2 = text2[commonlength:] | |
|
120 | ||
|
121 | # Trim off common suffix (speedup). | |
|
122 | commonlength = self.diff_commonSuffix(text1, text2) | |
|
123 | if commonlength == 0: | |
|
124 | commonsuffix = '' | |
|
125 | else: | |
|
126 | commonsuffix = text1[-commonlength:] | |
|
127 | text1 = text1[:-commonlength] | |
|
128 | text2 = text2[:-commonlength] | |
|
129 | ||
|
130 | # Compute the diff on the middle block. | |
|
131 | diffs = self.diff_compute(text1, text2, checklines, deadline) | |
|
132 | ||
|
133 | # Restore the prefix and suffix. | |
|
134 | if commonprefix: | |
|
135 | diffs[:0] = [(self.DIFF_EQUAL, commonprefix)] | |
|
136 | if commonsuffix: | |
|
137 | diffs.append((self.DIFF_EQUAL, commonsuffix)) | |
|
138 | self.diff_cleanupMerge(diffs) | |
|
139 | return diffs | |
|
140 | ||
|
141 | def diff_compute(self, text1, text2, checklines, deadline): | |
|
142 | """Find the differences between two texts. Assumes that the texts do not | |
|
143 | have any common prefix or suffix. | |
|
144 | ||
|
145 | Args: | |
|
146 | text1: Old string to be diffed. | |
|
147 | text2: New string to be diffed. | |
|
148 | checklines: Speedup flag. If false, then don't run a line-level diff | |
|
149 | first to identify the changed areas. | |
|
150 | If true, then run a faster, slightly less optimal diff. | |
|
151 | deadline: Time when the diff should be complete by. | |
|
152 | ||
|
153 | Returns: | |
|
154 | Array of changes. | |
|
155 | """ | |
|
156 | if not text1: | |
|
157 | # Just add some text (speedup). | |
|
158 | return [(self.DIFF_INSERT, text2)] | |
|
159 | ||
|
160 | if not text2: | |
|
161 | # Just delete some text (speedup). | |
|
162 | return [(self.DIFF_DELETE, text1)] | |
|
163 | ||
|
164 | if len(text1) > len(text2): | |
|
165 | (longtext, shorttext) = (text1, text2) | |
|
166 | else: | |
|
167 | (shorttext, longtext) = (text1, text2) | |
|
168 | i = longtext.find(shorttext) | |
|
169 | if i != -1: | |
|
170 | # Shorter text is inside the longer text (speedup). | |
|
171 | diffs = [(self.DIFF_INSERT, longtext[:i]), (self.DIFF_EQUAL, shorttext), | |
|
172 | (self.DIFF_INSERT, longtext[i + len(shorttext):])] | |
|
173 | # Swap insertions for deletions if diff is reversed. | |
|
174 | if len(text1) > len(text2): | |
|
175 | diffs[0] = (self.DIFF_DELETE, diffs[0][1]) | |
|
176 | diffs[2] = (self.DIFF_DELETE, diffs[2][1]) | |
|
177 | return diffs | |
|
178 | ||
|
179 | if len(shorttext) == 1: | |
|
180 | # Single character string. | |
|
181 | # After the previous speedup, the character can't be an equality. | |
|
182 | return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)] | |
|
183 | ||
|
184 | # Check to see if the problem can be split in two. | |
|
185 | hm = self.diff_halfMatch(text1, text2) | |
|
186 | if hm: | |
|
187 | # A half-match was found, sort out the return data. | |
|
188 | (text1_a, text1_b, text2_a, text2_b, mid_common) = hm | |
|
189 | # Send both pairs off for separate processing. | |
|
190 | diffs_a = self.diff_main(text1_a, text2_a, checklines, deadline) | |
|
191 | diffs_b = self.diff_main(text1_b, text2_b, checklines, deadline) | |
|
192 | # Merge the results. | |
|
193 | return diffs_a + [(self.DIFF_EQUAL, mid_common)] + diffs_b | |
|
194 | ||
|
195 | if checklines and len(text1) > 100 and len(text2) > 100: | |
|
196 | return self.diff_lineMode(text1, text2, deadline) | |
|
197 | ||
|
198 | return self.diff_bisect(text1, text2, deadline) | |
|
199 | ||
|
200 | def diff_lineMode(self, text1, text2, deadline): | |
|
201 | """Do a quick line-level diff on both strings, then rediff the parts for | |
|
202 | greater accuracy. | |
|
203 | This speedup can produce non-minimal diffs. | |
|
204 | ||
|
205 | Args: | |
|
206 | text1: Old string to be diffed. | |
|
207 | text2: New string to be diffed. | |
|
208 | deadline: Time when the diff should be complete by. | |
|
209 | ||
|
210 | Returns: | |
|
211 | Array of changes. | |
|
212 | """ | |
|
213 | ||
|
214 | # Scan the text on a line-by-line basis first. | |
|
215 | (text1, text2, linearray) = self.diff_linesToChars(text1, text2) | |
|
216 | ||
|
217 | diffs = self.diff_main(text1, text2, False, deadline) | |
|
218 | ||
|
219 | # Convert the diff back to original text. | |
|
220 | self.diff_charsToLines(diffs, linearray) | |
|
221 | # Eliminate freak matches (e.g. blank lines) | |
|
222 | self.diff_cleanupSemantic(diffs) | |
|
223 | ||
|
224 | # Rediff any replacement blocks, this time character-by-character. | |
|
225 | # Add a dummy entry at the end. | |
|
226 | diffs.append((self.DIFF_EQUAL, '')) | |
|
227 | pointer = 0 | |
|
228 | count_delete = 0 | |
|
229 | count_insert = 0 | |
|
230 | text_delete = '' | |
|
231 | text_insert = '' | |
|
232 | while pointer < len(diffs): | |
|
233 | if diffs[pointer][0] == self.DIFF_INSERT: | |
|
234 | count_insert += 1 | |
|
235 | text_insert += diffs[pointer][1] | |
|
236 | elif diffs[pointer][0] == self.DIFF_DELETE: | |
|
237 | count_delete += 1 | |
|
238 | text_delete += diffs[pointer][1] | |
|
239 | elif diffs[pointer][0] == self.DIFF_EQUAL: | |
|
240 | # Upon reaching an equality, check for prior redundancies. | |
|
241 | if count_delete >= 1 and count_insert >= 1: | |
|
242 | # Delete the offending records and add the merged ones. | |
|
243 | a = self.diff_main(text_delete, text_insert, False, deadline) | |
|
244 | diffs[pointer - count_delete - count_insert : pointer] = a | |
|
245 | pointer = pointer - count_delete - count_insert + len(a) | |
|
246 | count_insert = 0 | |
|
247 | count_delete = 0 | |
|
248 | text_delete = '' | |
|
249 | text_insert = '' | |
|
250 | ||
|
251 | pointer += 1 | |
|
252 | ||
|
253 | diffs.pop() # Remove the dummy entry at the end. | |
|
254 | ||
|
255 | return diffs | |
|
256 | ||
|
257 | def diff_bisect(self, text1, text2, deadline): | |
|
258 | """Find the 'middle snake' of a diff, split the problem in two | |
|
259 | and return the recursively constructed diff. | |
|
260 | See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. | |
|
261 | ||
|
262 | Args: | |
|
263 | text1: Old string to be diffed. | |
|
264 | text2: New string to be diffed. | |
|
265 | deadline: Time at which to bail if not yet complete. | |
|
266 | ||
|
267 | Returns: | |
|
268 | Array of diff tuples. | |
|
269 | """ | |
|
270 | ||
|
271 | # Cache the text lengths to prevent multiple calls. | |
|
272 | text1_length = len(text1) | |
|
273 | text2_length = len(text2) | |
|
274 | max_d = (text1_length + text2_length + 1) // 2 | |
|
275 | v_offset = max_d | |
|
276 | v_length = 2 * max_d | |
|
277 | v1 = [-1] * v_length | |
|
278 | v1[v_offset + 1] = 0 | |
|
279 | v2 = v1[:] | |
|
280 | delta = text1_length - text2_length | |
|
281 | # If the total number of characters is odd, then the front path will | |
|
282 | # collide with the reverse path. | |
|
283 | front = (delta % 2 != 0) | |
|
284 | # Offsets for start and end of k loop. | |
|
285 | # Prevents mapping of space beyond the grid. | |
|
286 | k1start = 0 | |
|
287 | k1end = 0 | |
|
288 | k2start = 0 | |
|
289 | k2end = 0 | |
|
290 | for d in xrange(max_d): | |
|
291 | # Bail out if deadline is reached. | |
|
292 | if time.time() > deadline: | |
|
293 | break | |
|
294 | ||
|
295 | # Walk the front path one step. | |
|
296 | for k1 in xrange(-d + k1start, d + 1 - k1end, 2): | |
|
297 | k1_offset = v_offset + k1 | |
|
298 | if k1 == -d or (k1 != d and | |
|
299 | v1[k1_offset - 1] < v1[k1_offset + 1]): | |
|
300 | x1 = v1[k1_offset + 1] | |
|
301 | else: | |
|
302 | x1 = v1[k1_offset - 1] + 1 | |
|
303 | y1 = x1 - k1 | |
|
304 | while (x1 < text1_length and y1 < text2_length and | |
|
305 | text1[x1] == text2[y1]): | |
|
306 | x1 += 1 | |
|
307 | y1 += 1 | |
|
308 | v1[k1_offset] = x1 | |
|
309 | if x1 > text1_length: | |
|
310 | # Ran off the right of the graph. | |
|
311 | k1end += 2 | |
|
312 | elif y1 > text2_length: | |
|
313 | # Ran off the bottom of the graph. | |
|
314 | k1start += 2 | |
|
315 | elif front: | |
|
316 | k2_offset = v_offset + delta - k1 | |
|
317 | if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] != -1: | |
|
318 | # Mirror x2 onto top-left coordinate system. | |
|
319 | x2 = text1_length - v2[k2_offset] | |
|
320 | if x1 >= x2: | |
|
321 | # Overlap detected. | |
|
322 | return self.diff_bisectSplit(text1, text2, x1, y1, deadline) | |
|
323 | ||
|
324 | # Walk the reverse path one step. | |
|
325 | for k2 in xrange(-d + k2start, d + 1 - k2end, 2): | |
|
326 | k2_offset = v_offset + k2 | |
|
327 | if k2 == -d or (k2 != d and | |
|
328 | v2[k2_offset - 1] < v2[k2_offset + 1]): | |
|
329 | x2 = v2[k2_offset + 1] | |
|
330 | else: | |
|
331 | x2 = v2[k2_offset - 1] + 1 | |
|
332 | y2 = x2 - k2 | |
|
333 | while (x2 < text1_length and y2 < text2_length and | |
|
334 | text1[-x2 - 1] == text2[-y2 - 1]): | |
|
335 | x2 += 1 | |
|
336 | y2 += 1 | |
|
337 | v2[k2_offset] = x2 | |
|
338 | if x2 > text1_length: | |
|
339 | # Ran off the left of the graph. | |
|
340 | k2end += 2 | |
|
341 | elif y2 > text2_length: | |
|
342 | # Ran off the top of the graph. | |
|
343 | k2start += 2 | |
|
344 | elif not front: | |
|
345 | k1_offset = v_offset + delta - k2 | |
|
346 | if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] != -1: | |
|
347 | x1 = v1[k1_offset] | |
|
348 | y1 = v_offset + x1 - k1_offset | |
|
349 | # Mirror x2 onto top-left coordinate system. | |
|
350 | x2 = text1_length - x2 | |
|
351 | if x1 >= x2: | |
|
352 | # Overlap detected. | |
|
353 | return self.diff_bisectSplit(text1, text2, x1, y1, deadline) | |
|
354 | ||
|
355 | # Diff took too long and hit the deadline or | |
|
356 | # number of diffs equals number of characters, no commonality at all. | |
|
357 | return [(self.DIFF_DELETE, text1), (self.DIFF_INSERT, text2)] | |
|
358 | ||
|
359 | def diff_bisectSplit(self, text1, text2, x, y, deadline): | |
|
360 | """Given the location of the 'middle snake', split the diff in two parts | |
|
361 | and recurse. | |
|
362 | ||
|
363 | Args: | |
|
364 | text1: Old string to be diffed. | |
|
365 | text2: New string to be diffed. | |
|
366 | x: Index of split point in text1. | |
|
367 | y: Index of split point in text2. | |
|
368 | deadline: Time at which to bail if not yet complete. | |
|
369 | ||
|
370 | Returns: | |
|
371 | Array of diff tuples. | |
|
372 | """ | |
|
373 | text1a = text1[:x] | |
|
374 | text2a = text2[:y] | |
|
375 | text1b = text1[x:] | |
|
376 | text2b = text2[y:] | |
|
377 | ||
|
378 | # Compute both diffs serially. | |
|
379 | diffs = self.diff_main(text1a, text2a, False, deadline) | |
|
380 | diffsb = self.diff_main(text1b, text2b, False, deadline) | |
|
381 | ||
|
382 | return diffs + diffsb | |
|
383 | ||
|
384 | def diff_linesToChars(self, text1, text2): | |
|
385 | """Split two texts into an array of strings. Reduce the texts to a string | |
|
386 | of hashes where each Unicode character represents one line. | |
|
387 | ||
|
388 | Args: | |
|
389 | text1: First string. | |
|
390 | text2: Second string. | |
|
391 | ||
|
392 | Returns: | |
|
393 | Three element tuple, containing the encoded text1, the encoded text2 and | |
|
394 | the array of unique strings. The zeroth element of the array of unique | |
|
395 | strings is intentionally blank. | |
|
396 | """ | |
|
397 | lineArray = [] # e.g. lineArray[4] == "Hello\n" | |
|
398 | lineHash = {} # e.g. lineHash["Hello\n"] == 4 | |
|
399 | ||
|
400 | # "\x00" is a valid character, but various debuggers don't like it. | |
|
401 | # So we'll insert a junk entry to avoid generating a null character. | |
|
402 | lineArray.append('') | |
|
403 | ||
|
404 | def diff_linesToCharsMunge(text): | |
|
405 | """Split a text into an array of strings. Reduce the texts to a string | |
|
406 | of hashes where each Unicode character represents one line. | |
|
407 | Modifies linearray and linehash through being a closure. | |
|
408 | ||
|
409 | Args: | |
|
410 | text: String to encode. | |
|
411 | ||
|
412 | Returns: | |
|
413 | Encoded string. | |
|
414 | """ | |
|
415 | chars = [] | |
|
416 | # Walk the text, pulling out a substring for each line. | |
|
417 | # text.split('\n') would would temporarily double our memory footprint. | |
|
418 | # Modifying text would create many large strings to garbage collect. | |
|
419 | lineStart = 0 | |
|
420 | lineEnd = -1 | |
|
421 | while lineEnd < len(text) - 1: | |
|
422 | lineEnd = text.find('\n', lineStart) | |
|
423 | if lineEnd == -1: | |
|
424 | lineEnd = len(text) - 1 | |
|
425 | line = text[lineStart:lineEnd + 1] | |
|
426 | lineStart = lineEnd + 1 | |
|
427 | ||
|
428 | if line in lineHash: | |
|
429 | chars.append(unichr(lineHash[line])) | |
|
430 | else: | |
|
431 | lineArray.append(line) | |
|
432 | lineHash[line] = len(lineArray) - 1 | |
|
433 | chars.append(unichr(len(lineArray) - 1)) | |
|
434 | return "".join(chars) | |
|
435 | ||
|
436 | chars1 = diff_linesToCharsMunge(text1) | |
|
437 | chars2 = diff_linesToCharsMunge(text2) | |
|
438 | return (chars1, chars2, lineArray) | |
|
439 | ||
|
440 | def diff_charsToLines(self, diffs, lineArray): | |
|
441 | """Rehydrate the text in a diff from a string of line hashes to real lines | |
|
442 | of text. | |
|
443 | ||
|
444 | Args: | |
|
445 | diffs: Array of diff tuples. | |
|
446 | lineArray: Array of unique strings. | |
|
447 | """ | |
|
448 | for x in xrange(len(diffs)): | |
|
449 | text = [] | |
|
450 | for char in diffs[x][1]: | |
|
451 | text.append(lineArray[ord(char)]) | |
|
452 | diffs[x] = (diffs[x][0], "".join(text)) | |
|
453 | ||
|
454 | def diff_commonPrefix(self, text1, text2): | |
|
455 | """Determine the common prefix of two strings. | |
|
456 | ||
|
457 | Args: | |
|
458 | text1: First string. | |
|
459 | text2: Second string. | |
|
460 | ||
|
461 | Returns: | |
|
462 | The number of characters common to the start of each string. | |
|
463 | """ | |
|
464 | # Quick check for common null cases. | |
|
465 | if not text1 or not text2 or text1[0] != text2[0]: | |
|
466 | return 0 | |
|
467 | # Binary search. | |
|
468 | # Performance analysis: http://neil.fraser.name/news/2007/10/09/ | |
|
469 | pointermin = 0 | |
|
470 | pointermax = min(len(text1), len(text2)) | |
|
471 | pointermid = pointermax | |
|
472 | pointerstart = 0 | |
|
473 | while pointermin < pointermid: | |
|
474 | if text1[pointerstart:pointermid] == text2[pointerstart:pointermid]: | |
|
475 | pointermin = pointermid | |
|
476 | pointerstart = pointermin | |
|
477 | else: | |
|
478 | pointermax = pointermid | |
|
479 | pointermid = (pointermax - pointermin) // 2 + pointermin | |
|
480 | return pointermid | |
|
481 | ||
|
482 | def diff_commonSuffix(self, text1, text2): | |
|
483 | """Determine the common suffix of two strings. | |
|
484 | ||
|
485 | Args: | |
|
486 | text1: First string. | |
|
487 | text2: Second string. | |
|
488 | ||
|
489 | Returns: | |
|
490 | The number of characters common to the end of each string. | |
|
491 | """ | |
|
492 | # Quick check for common null cases. | |
|
493 | if not text1 or not text2 or text1[-1] != text2[-1]: | |
|
494 | return 0 | |
|
495 | # Binary search. | |
|
496 | # Performance analysis: http://neil.fraser.name/news/2007/10/09/ | |
|
497 | pointermin = 0 | |
|
498 | pointermax = min(len(text1), len(text2)) | |
|
499 | pointermid = pointermax | |
|
500 | pointerend = 0 | |
|
501 | while pointermin < pointermid: | |
|
502 | if (text1[-pointermid:len(text1) - pointerend] == | |
|
503 | text2[-pointermid:len(text2) - pointerend]): | |
|
504 | pointermin = pointermid | |
|
505 | pointerend = pointermin | |
|
506 | else: | |
|
507 | pointermax = pointermid | |
|
508 | pointermid = (pointermax - pointermin) // 2 + pointermin | |
|
509 | return pointermid | |
|
510 | ||
|
511 | def diff_commonOverlap(self, text1, text2): | |
|
512 | """Determine if the suffix of one string is the prefix of another. | |
|
513 | ||
|
514 | Args: | |
|
515 | text1 First string. | |
|
516 | text2 Second string. | |
|
517 | ||
|
518 | Returns: | |
|
519 | The number of characters common to the end of the first | |
|
520 | string and the start of the second string. | |
|
521 | """ | |
|
522 | # Cache the text lengths to prevent multiple calls. | |
|
523 | text1_length = len(text1) | |
|
524 | text2_length = len(text2) | |
|
525 | # Eliminate the null case. | |
|
526 | if text1_length == 0 or text2_length == 0: | |
|
527 | return 0 | |
|
528 | # Truncate the longer string. | |
|
529 | if text1_length > text2_length: | |
|
530 | text1 = text1[-text2_length:] | |
|
531 | elif text1_length < text2_length: | |
|
532 | text2 = text2[:text1_length] | |
|
533 | text_length = min(text1_length, text2_length) | |
|
534 | # Quick check for the worst case. | |
|
535 | if text1 == text2: | |
|
536 | return text_length | |
|
537 | ||
|
538 | # Start by looking for a single character match | |
|
539 | # and increase length until no match is found. | |
|
540 | # Performance analysis: http://neil.fraser.name/news/2010/11/04/ | |
|
541 | best = 0 | |
|
542 | length = 1 | |
|
543 | while True: | |
|
544 | pattern = text1[-length:] | |
|
545 | found = text2.find(pattern) | |
|
546 | if found == -1: | |
|
547 | return best | |
|
548 | length += found | |
|
549 | if found == 0 or text1[-length:] == text2[:length]: | |
|
550 | best = length | |
|
551 | length += 1 | |
|
552 | ||
|
553 | def diff_halfMatch(self, text1, text2): | |
|
554 | """Do the two texts share a substring which is at least half the length of | |
|
555 | the longer text? | |
|
556 | This speedup can produce non-minimal diffs. | |
|
557 | ||
|
558 | Args: | |
|
559 | text1: First string. | |
|
560 | text2: Second string. | |
|
561 | ||
|
562 | Returns: | |
|
563 | Five element Array, containing the prefix of text1, the suffix of text1, | |
|
564 | the prefix of text2, the suffix of text2 and the common middle. Or None | |
|
565 | if there was no match. | |
|
566 | """ | |
|
567 | if self.Diff_Timeout <= 0: | |
|
568 | # Don't risk returning a non-optimal diff if we have unlimited time. | |
|
569 | return None | |
|
570 | if len(text1) > len(text2): | |
|
571 | (longtext, shorttext) = (text1, text2) | |
|
572 | else: | |
|
573 | (shorttext, longtext) = (text1, text2) | |
|
574 | if len(longtext) < 4 or len(shorttext) * 2 < len(longtext): | |
|
575 | return None # Pointless. | |
|
576 | ||
|
577 | def diff_halfMatchI(longtext, shorttext, i): | |
|
578 | """Does a substring of shorttext exist within longtext such that the | |
|
579 | substring is at least half the length of longtext? | |
|
580 | Closure, but does not reference any external variables. | |
|
581 | ||
|
582 | Args: | |
|
583 | longtext: Longer string. | |
|
584 | shorttext: Shorter string. | |
|
585 | i: Start index of quarter length substring within longtext. | |
|
586 | ||
|
587 | Returns: | |
|
588 | Five element Array, containing the prefix of longtext, the suffix of | |
|
589 | longtext, the prefix of shorttext, the suffix of shorttext and the | |
|
590 | common middle. Or None if there was no match. | |
|
591 | """ | |
|
592 | seed = longtext[i:i + len(longtext) // 4] | |
|
593 | best_common = '' | |
|
594 | j = shorttext.find(seed) | |
|
595 | while j != -1: | |
|
596 | prefixLength = self.diff_commonPrefix(longtext[i:], shorttext[j:]) | |
|
597 | suffixLength = self.diff_commonSuffix(longtext[:i], shorttext[:j]) | |
|
598 | if len(best_common) < suffixLength + prefixLength: | |
|
599 | best_common = (shorttext[j - suffixLength:j] + | |
|
600 | shorttext[j:j + prefixLength]) | |
|
601 | best_longtext_a = longtext[:i - suffixLength] | |
|
602 | best_longtext_b = longtext[i + prefixLength:] | |
|
603 | best_shorttext_a = shorttext[:j - suffixLength] | |
|
604 | best_shorttext_b = shorttext[j + prefixLength:] | |
|
605 | j = shorttext.find(seed, j + 1) | |
|
606 | ||
|
607 | if len(best_common) * 2 >= len(longtext): | |
|
608 | return (best_longtext_a, best_longtext_b, | |
|
609 | best_shorttext_a, best_shorttext_b, best_common) | |
|
610 | else: | |
|
611 | return None | |
|
612 | ||
|
613 | # First check if the second quarter is the seed for a half-match. | |
|
614 | hm1 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 3) // 4) | |
|
615 | # Check again based on the third quarter. | |
|
616 | hm2 = diff_halfMatchI(longtext, shorttext, (len(longtext) + 1) // 2) | |
|
617 | if not hm1 and not hm2: | |
|
618 | return None | |
|
619 | elif not hm2: | |
|
620 | hm = hm1 | |
|
621 | elif not hm1: | |
|
622 | hm = hm2 | |
|
623 | else: | |
|
624 | # Both matched. Select the longest. | |
|
625 | if len(hm1[4]) > len(hm2[4]): | |
|
626 | hm = hm1 | |
|
627 | else: | |
|
628 | hm = hm2 | |
|
629 | ||
|
630 | # A half-match was found, sort out the return data. | |
|
631 | if len(text1) > len(text2): | |
|
632 | (text1_a, text1_b, text2_a, text2_b, mid_common) = hm | |
|
633 | else: | |
|
634 | (text2_a, text2_b, text1_a, text1_b, mid_common) = hm | |
|
635 | return (text1_a, text1_b, text2_a, text2_b, mid_common) | |
|
636 | ||
|
637 | def diff_cleanupSemantic(self, diffs): | |
|
638 | """Reduce the number of edits by eliminating semantically trivial | |
|
639 | equalities. | |
|
640 | ||
|
641 | Args: | |
|
642 | diffs: Array of diff tuples. | |
|
643 | """ | |
|
644 | changes = False | |
|
645 | equalities = [] # Stack of indices where equalities are found. | |
|
646 | lastequality = None # Always equal to diffs[equalities[-1]][1] | |
|
647 | pointer = 0 # Index of current position. | |
|
648 | # Number of chars that changed prior to the equality. | |
|
649 | length_insertions1, length_deletions1 = 0, 0 | |
|
650 | # Number of chars that changed after the equality. | |
|
651 | length_insertions2, length_deletions2 = 0, 0 | |
|
652 | while pointer < len(diffs): | |
|
653 | if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found. | |
|
654 | equalities.append(pointer) | |
|
655 | length_insertions1, length_insertions2 = length_insertions2, 0 | |
|
656 | length_deletions1, length_deletions2 = length_deletions2, 0 | |
|
657 | lastequality = diffs[pointer][1] | |
|
658 | else: # An insertion or deletion. | |
|
659 | if diffs[pointer][0] == self.DIFF_INSERT: | |
|
660 | length_insertions2 += len(diffs[pointer][1]) | |
|
661 | else: | |
|
662 | length_deletions2 += len(diffs[pointer][1]) | |
|
663 | # Eliminate an equality that is smaller or equal to the edits on both | |
|
664 | # sides of it. | |
|
665 | if (lastequality and (len(lastequality) <= | |
|
666 | max(length_insertions1, length_deletions1)) and | |
|
667 | (len(lastequality) <= max(length_insertions2, length_deletions2))): | |
|
668 | # Duplicate record. | |
|
669 | diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality)) | |
|
670 | # Change second copy to insert. | |
|
671 | diffs[equalities[-1] + 1] = (self.DIFF_INSERT, | |
|
672 | diffs[equalities[-1] + 1][1]) | |
|
673 | # Throw away the equality we just deleted. | |
|
674 | equalities.pop() | |
|
675 | # Throw away the previous equality (it needs to be reevaluated). | |
|
676 | if len(equalities): | |
|
677 | equalities.pop() | |
|
678 | if len(equalities): | |
|
679 | pointer = equalities[-1] | |
|
680 | else: | |
|
681 | pointer = -1 | |
|
682 | # Reset the counters. | |
|
683 | length_insertions1, length_deletions1 = 0, 0 | |
|
684 | length_insertions2, length_deletions2 = 0, 0 | |
|
685 | lastequality = None | |
|
686 | changes = True | |
|
687 | pointer += 1 | |
|
688 | ||
|
689 | # Normalize the diff. | |
|
690 | if changes: | |
|
691 | self.diff_cleanupMerge(diffs) | |
|
692 | self.diff_cleanupSemanticLossless(diffs) | |
|
693 | ||
|
694 | # Find any overlaps between deletions and insertions. | |
|
695 | # e.g: <del>abcxxx</del><ins>xxxdef</ins> | |
|
696 | # -> <del>abc</del>xxx<ins>def</ins> | |
|
697 | # e.g: <del>xxxabc</del><ins>defxxx</ins> | |
|
698 | # -> <ins>def</ins>xxx<del>abc</del> | |
|
699 | # Only extract an overlap if it is as big as the edit ahead or behind it. | |
|
700 | pointer = 1 | |
|
701 | while pointer < len(diffs): | |
|
702 | if (diffs[pointer - 1][0] == self.DIFF_DELETE and | |
|
703 | diffs[pointer][0] == self.DIFF_INSERT): | |
|
704 | deletion = diffs[pointer - 1][1] | |
|
705 | insertion = diffs[pointer][1] | |
|
706 | overlap_length1 = self.diff_commonOverlap(deletion, insertion) | |
|
707 | overlap_length2 = self.diff_commonOverlap(insertion, deletion) | |
|
708 | if overlap_length1 >= overlap_length2: | |
|
709 | if (overlap_length1 >= len(deletion) / 2.0 or | |
|
710 | overlap_length1 >= len(insertion) / 2.0): | |
|
711 | # Overlap found. Insert an equality and trim the surrounding edits. | |
|
712 | diffs.insert(pointer, (self.DIFF_EQUAL, | |
|
713 | insertion[:overlap_length1])) | |
|
714 | diffs[pointer - 1] = (self.DIFF_DELETE, | |
|
715 | deletion[:len(deletion) - overlap_length1]) | |
|
716 | diffs[pointer + 1] = (self.DIFF_INSERT, | |
|
717 | insertion[overlap_length1:]) | |
|
718 | pointer += 1 | |
|
719 | else: | |
|
720 | if (overlap_length2 >= len(deletion) / 2.0 or | |
|
721 | overlap_length2 >= len(insertion) / 2.0): | |
|
722 | # Reverse overlap found. | |
|
723 | # Insert an equality and swap and trim the surrounding edits. | |
|
724 | diffs.insert(pointer, (self.DIFF_EQUAL, deletion[:overlap_length2])) | |
|
725 | diffs[pointer - 1] = (self.DIFF_INSERT, | |
|
726 | insertion[:len(insertion) - overlap_length2]) | |
|
727 | diffs[pointer + 1] = (self.DIFF_DELETE, deletion[overlap_length2:]) | |
|
728 | pointer += 1 | |
|
729 | pointer += 1 | |
|
730 | pointer += 1 | |
|
731 | ||
|
732 | def diff_cleanupSemanticLossless(self, diffs): | |
|
733 | """Look for single edits surrounded on both sides by equalities | |
|
734 | which can be shifted sideways to align the edit to a word boundary. | |
|
735 | e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came. | |
|
736 | ||
|
737 | Args: | |
|
738 | diffs: Array of diff tuples. | |
|
739 | """ | |
|
740 | ||
|
741 | def diff_cleanupSemanticScore(one, two): | |
|
742 | """Given two strings, compute a score representing whether the | |
|
743 | internal boundary falls on logical boundaries. | |
|
744 | Scores range from 6 (best) to 0 (worst). | |
|
745 | Closure, but does not reference any external variables. | |
|
746 | ||
|
747 | Args: | |
|
748 | one: First string. | |
|
749 | two: Second string. | |
|
750 | ||
|
751 | Returns: | |
|
752 | The score. | |
|
753 | """ | |
|
754 | if not one or not two: | |
|
755 | # Edges are the best. | |
|
756 | return 6 | |
|
757 | ||
|
758 | # Each port of this function behaves slightly differently due to | |
|
759 | # subtle differences in each language's definition of things like | |
|
760 | # 'whitespace'. Since this function's purpose is largely cosmetic, | |
|
761 | # the choice has been made to use each language's native features | |
|
762 | # rather than force total conformity. | |
|
763 | char1 = one[-1] | |
|
764 | char2 = two[0] | |
|
765 | nonAlphaNumeric1 = not char1.isalnum() | |
|
766 | nonAlphaNumeric2 = not char2.isalnum() | |
|
767 | whitespace1 = nonAlphaNumeric1 and char1.isspace() | |
|
768 | whitespace2 = nonAlphaNumeric2 and char2.isspace() | |
|
769 | lineBreak1 = whitespace1 and (char1 == "\r" or char1 == "\n") | |
|
770 | lineBreak2 = whitespace2 and (char2 == "\r" or char2 == "\n") | |
|
771 | blankLine1 = lineBreak1 and self.BLANKLINEEND.search(one) | |
|
772 | blankLine2 = lineBreak2 and self.BLANKLINESTART.match(two) | |
|
773 | ||
|
774 | if blankLine1 or blankLine2: | |
|
775 | # Five points for blank lines. | |
|
776 | return 5 | |
|
777 | elif lineBreak1 or lineBreak2: | |
|
778 | # Four points for line breaks. | |
|
779 | return 4 | |
|
780 | elif nonAlphaNumeric1 and not whitespace1 and whitespace2: | |
|
781 | # Three points for end of sentences. | |
|
782 | return 3 | |
|
783 | elif whitespace1 or whitespace2: | |
|
784 | # Two points for whitespace. | |
|
785 | return 2 | |
|
786 | elif nonAlphaNumeric1 or nonAlphaNumeric2: | |
|
787 | # One point for non-alphanumeric. | |
|
788 | return 1 | |
|
789 | return 0 | |
|
790 | ||
|
791 | pointer = 1 | |
|
792 | # Intentionally ignore the first and last element (don't need checking). | |
|
793 | while pointer < len(diffs) - 1: | |
|
794 | if (diffs[pointer - 1][0] == self.DIFF_EQUAL and | |
|
795 | diffs[pointer + 1][0] == self.DIFF_EQUAL): | |
|
796 | # This is a single edit surrounded by equalities. | |
|
797 | equality1 = diffs[pointer - 1][1] | |
|
798 | edit = diffs[pointer][1] | |
|
799 | equality2 = diffs[pointer + 1][1] | |
|
800 | ||
|
801 | # First, shift the edit as far left as possible. | |
|
802 | commonOffset = self.diff_commonSuffix(equality1, edit) | |
|
803 | if commonOffset: | |
|
804 | commonString = edit[-commonOffset:] | |
|
805 | equality1 = equality1[:-commonOffset] | |
|
806 | edit = commonString + edit[:-commonOffset] | |
|
807 | equality2 = commonString + equality2 | |
|
808 | ||
|
809 | # Second, step character by character right, looking for the best fit. | |
|
810 | bestEquality1 = equality1 | |
|
811 | bestEdit = edit | |
|
812 | bestEquality2 = equality2 | |
|
813 | bestScore = (diff_cleanupSemanticScore(equality1, edit) + | |
|
814 | diff_cleanupSemanticScore(edit, equality2)) | |
|
815 | while edit and equality2 and edit[0] == equality2[0]: | |
|
816 | equality1 += edit[0] | |
|
817 | edit = edit[1:] + equality2[0] | |
|
818 | equality2 = equality2[1:] | |
|
819 | score = (diff_cleanupSemanticScore(equality1, edit) + | |
|
820 | diff_cleanupSemanticScore(edit, equality2)) | |
|
821 | # The >= encourages trailing rather than leading whitespace on edits. | |
|
822 | if score >= bestScore: | |
|
823 | bestScore = score | |
|
824 | bestEquality1 = equality1 | |
|
825 | bestEdit = edit | |
|
826 | bestEquality2 = equality2 | |
|
827 | ||
|
828 | if diffs[pointer - 1][1] != bestEquality1: | |
|
829 | # We have an improvement, save it back to the diff. | |
|
830 | if bestEquality1: | |
|
831 | diffs[pointer - 1] = (diffs[pointer - 1][0], bestEquality1) | |
|
832 | else: | |
|
833 | del diffs[pointer - 1] | |
|
834 | pointer -= 1 | |
|
835 | diffs[pointer] = (diffs[pointer][0], bestEdit) | |
|
836 | if bestEquality2: | |
|
837 | diffs[pointer + 1] = (diffs[pointer + 1][0], bestEquality2) | |
|
838 | else: | |
|
839 | del diffs[pointer + 1] | |
|
840 | pointer -= 1 | |
|
841 | pointer += 1 | |
|
842 | ||
|
843 | # Define some regex patterns for matching boundaries. | |
|
844 | BLANKLINEEND = re.compile(r"\n\r?\n$"); | |
|
845 | BLANKLINESTART = re.compile(r"^\r?\n\r?\n"); | |
|
846 | ||
|
847 | def diff_cleanupEfficiency(self, diffs): | |
|
848 | """Reduce the number of edits by eliminating operationally trivial | |
|
849 | equalities. | |
|
850 | ||
|
851 | Args: | |
|
852 | diffs: Array of diff tuples. | |
|
853 | """ | |
|
854 | changes = False | |
|
855 | equalities = [] # Stack of indices where equalities are found. | |
|
856 | lastequality = None # Always equal to diffs[equalities[-1]][1] | |
|
857 | pointer = 0 # Index of current position. | |
|
858 | pre_ins = False # Is there an insertion operation before the last equality. | |
|
859 | pre_del = False # Is there a deletion operation before the last equality. | |
|
860 | post_ins = False # Is there an insertion operation after the last equality. | |
|
861 | post_del = False # Is there a deletion operation after the last equality. | |
|
862 | while pointer < len(diffs): | |
|
863 | if diffs[pointer][0] == self.DIFF_EQUAL: # Equality found. | |
|
864 | if (len(diffs[pointer][1]) < self.Diff_EditCost and | |
|
865 | (post_ins or post_del)): | |
|
866 | # Candidate found. | |
|
867 | equalities.append(pointer) | |
|
868 | pre_ins = post_ins | |
|
869 | pre_del = post_del | |
|
870 | lastequality = diffs[pointer][1] | |
|
871 | else: | |
|
872 | # Not a candidate, and can never become one. | |
|
873 | equalities = [] | |
|
874 | lastequality = None | |
|
875 | ||
|
876 | post_ins = post_del = False | |
|
877 | else: # An insertion or deletion. | |
|
878 | if diffs[pointer][0] == self.DIFF_DELETE: | |
|
879 | post_del = True | |
|
880 | else: | |
|
881 | post_ins = True | |
|
882 | ||
|
883 | # Five types to be split: | |
|
884 | # <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del> | |
|
885 | # <ins>A</ins>X<ins>C</ins><del>D</del> | |
|
886 | # <ins>A</ins><del>B</del>X<ins>C</ins> | |
|
887 | # <ins>A</del>X<ins>C</ins><del>D</del> | |
|
888 | # <ins>A</ins><del>B</del>X<del>C</del> | |
|
889 | ||
|
890 | if lastequality and ((pre_ins and pre_del and post_ins and post_del) or | |
|
891 | ((len(lastequality) < self.Diff_EditCost / 2) and | |
|
892 | (pre_ins + pre_del + post_ins + post_del) == 3)): | |
|
893 | # Duplicate record. | |
|
894 | diffs.insert(equalities[-1], (self.DIFF_DELETE, lastequality)) | |
|
895 | # Change second copy to insert. | |
|
896 | diffs[equalities[-1] + 1] = (self.DIFF_INSERT, | |
|
897 | diffs[equalities[-1] + 1][1]) | |
|
898 | equalities.pop() # Throw away the equality we just deleted. | |
|
899 | lastequality = None | |
|
900 | if pre_ins and pre_del: | |
|
901 | # No changes made which could affect previous entry, keep going. | |
|
902 | post_ins = post_del = True | |
|
903 | equalities = [] | |
|
904 | else: | |
|
905 | if len(equalities): | |
|
906 | equalities.pop() # Throw away the previous equality. | |
|
907 | if len(equalities): | |
|
908 | pointer = equalities[-1] | |
|
909 | else: | |
|
910 | pointer = -1 | |
|
911 | post_ins = post_del = False | |
|
912 | changes = True | |
|
913 | pointer += 1 | |
|
914 | ||
|
915 | if changes: | |
|
916 | self.diff_cleanupMerge(diffs) | |
|
917 | ||
|
918 | def diff_cleanupMerge(self, diffs): | |
|
919 | """Reorder and merge like edit sections. Merge equalities. | |
|
920 | Any edit section can move as long as it doesn't cross an equality. | |
|
921 | ||
|
922 | Args: | |
|
923 | diffs: Array of diff tuples. | |
|
924 | """ | |
|
925 | diffs.append((self.DIFF_EQUAL, '')) # Add a dummy entry at the end. | |
|
926 | pointer = 0 | |
|
927 | count_delete = 0 | |
|
928 | count_insert = 0 | |
|
929 | text_delete = '' | |
|
930 | text_insert = '' | |
|
931 | while pointer < len(diffs): | |
|
932 | if diffs[pointer][0] == self.DIFF_INSERT: | |
|
933 | count_insert += 1 | |
|
934 | text_insert += diffs[pointer][1] | |
|
935 | pointer += 1 | |
|
936 | elif diffs[pointer][0] == self.DIFF_DELETE: | |
|
937 | count_delete += 1 | |
|
938 | text_delete += diffs[pointer][1] | |
|
939 | pointer += 1 | |
|
940 | elif diffs[pointer][0] == self.DIFF_EQUAL: | |
|
941 | # Upon reaching an equality, check for prior redundancies. | |
|
942 | if count_delete + count_insert > 1: | |
|
943 | if count_delete != 0 and count_insert != 0: | |
|
944 | # Factor out any common prefixies. | |
|
945 | commonlength = self.diff_commonPrefix(text_insert, text_delete) | |
|
946 | if commonlength != 0: | |
|
947 | x = pointer - count_delete - count_insert - 1 | |
|
948 | if x >= 0 and diffs[x][0] == self.DIFF_EQUAL: | |
|
949 | diffs[x] = (diffs[x][0], diffs[x][1] + | |
|
950 | text_insert[:commonlength]) | |
|
951 | else: | |
|
952 | diffs.insert(0, (self.DIFF_EQUAL, text_insert[:commonlength])) | |
|
953 | pointer += 1 | |
|
954 | text_insert = text_insert[commonlength:] | |
|
955 | text_delete = text_delete[commonlength:] | |
|
956 | # Factor out any common suffixies. | |
|
957 | commonlength = self.diff_commonSuffix(text_insert, text_delete) | |
|
958 | if commonlength != 0: | |
|
959 | diffs[pointer] = (diffs[pointer][0], text_insert[-commonlength:] + | |
|
960 | diffs[pointer][1]) | |
|
961 | text_insert = text_insert[:-commonlength] | |
|
962 | text_delete = text_delete[:-commonlength] | |
|
963 | # Delete the offending records and add the merged ones. | |
|
964 | if count_delete == 0: | |
|
965 | diffs[pointer - count_insert : pointer] = [ | |
|
966 | (self.DIFF_INSERT, text_insert)] | |
|
967 | elif count_insert == 0: | |
|
968 | diffs[pointer - count_delete : pointer] = [ | |
|
969 | (self.DIFF_DELETE, text_delete)] | |
|
970 | else: | |
|
971 | diffs[pointer - count_delete - count_insert : pointer] = [ | |
|
972 | (self.DIFF_DELETE, text_delete), | |
|
973 | (self.DIFF_INSERT, text_insert)] | |
|
974 | pointer = pointer - count_delete - count_insert + 1 | |
|
975 | if count_delete != 0: | |
|
976 | pointer += 1 | |
|
977 | if count_insert != 0: | |
|
978 | pointer += 1 | |
|
979 | elif pointer != 0 and diffs[pointer - 1][0] == self.DIFF_EQUAL: | |
|
980 | # Merge this equality with the previous one. | |
|
981 | diffs[pointer - 1] = (diffs[pointer - 1][0], | |
|
982 | diffs[pointer - 1][1] + diffs[pointer][1]) | |
|
983 | del diffs[pointer] | |
|
984 | else: | |
|
985 | pointer += 1 | |
|
986 | ||
|
987 | count_insert = 0 | |
|
988 | count_delete = 0 | |
|
989 | text_delete = '' | |
|
990 | text_insert = '' | |
|
991 | ||
|
992 | if diffs[-1][1] == '': | |
|
993 | diffs.pop() # Remove the dummy entry at the end. | |
|
994 | ||
|
995 | # Second pass: look for single edits surrounded on both sides by equalities | |
|
996 | # which can be shifted sideways to eliminate an equality. | |
|
997 | # e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC | |
|
998 | changes = False | |
|
999 | pointer = 1 | |
|
1000 | # Intentionally ignore the first and last element (don't need checking). | |
|
1001 | while pointer < len(diffs) - 1: | |
|
1002 | if (diffs[pointer - 1][0] == self.DIFF_EQUAL and | |
|
1003 | diffs[pointer + 1][0] == self.DIFF_EQUAL): | |
|
1004 | # This is a single edit surrounded by equalities. | |
|
1005 | if diffs[pointer][1].endswith(diffs[pointer - 1][1]): | |
|
1006 | # Shift the edit over the previous equality. | |
|
1007 | diffs[pointer] = (diffs[pointer][0], | |
|
1008 | diffs[pointer - 1][1] + | |
|
1009 | diffs[pointer][1][:-len(diffs[pointer - 1][1])]) | |
|
1010 | diffs[pointer + 1] = (diffs[pointer + 1][0], | |
|
1011 | diffs[pointer - 1][1] + diffs[pointer + 1][1]) | |
|
1012 | del diffs[pointer - 1] | |
|
1013 | changes = True | |
|
1014 | elif diffs[pointer][1].startswith(diffs[pointer + 1][1]): | |
|
1015 | # Shift the edit over the next equality. | |
|
1016 | diffs[pointer - 1] = (diffs[pointer - 1][0], | |
|
1017 | diffs[pointer - 1][1] + diffs[pointer + 1][1]) | |
|
1018 | diffs[pointer] = (diffs[pointer][0], | |
|
1019 | diffs[pointer][1][len(diffs[pointer + 1][1]):] + | |
|
1020 | diffs[pointer + 1][1]) | |
|
1021 | del diffs[pointer + 1] | |
|
1022 | changes = True | |
|
1023 | pointer += 1 | |
|
1024 | ||
|
1025 | # If shifts were made, the diff needs reordering and another shift sweep. | |
|
1026 | if changes: | |
|
1027 | self.diff_cleanupMerge(diffs) | |
|
1028 | ||
|
1029 | def diff_xIndex(self, diffs, loc): | |
|
1030 | """loc is a location in text1, compute and return the equivalent location | |
|
1031 | in text2. e.g. "The cat" vs "The big cat", 1->1, 5->8 | |
|
1032 | ||
|
1033 | Args: | |
|
1034 | diffs: Array of diff tuples. | |
|
1035 | loc: Location within text1. | |
|
1036 | ||
|
1037 | Returns: | |
|
1038 | Location within text2. | |
|
1039 | """ | |
|
1040 | chars1 = 0 | |
|
1041 | chars2 = 0 | |
|
1042 | last_chars1 = 0 | |
|
1043 | last_chars2 = 0 | |
|
1044 | for x in xrange(len(diffs)): | |
|
1045 | (op, text) = diffs[x] | |
|
1046 | if op != self.DIFF_INSERT: # Equality or deletion. | |
|
1047 | chars1 += len(text) | |
|
1048 | if op != self.DIFF_DELETE: # Equality or insertion. | |
|
1049 | chars2 += len(text) | |
|
1050 | if chars1 > loc: # Overshot the location. | |
|
1051 | break | |
|
1052 | last_chars1 = chars1 | |
|
1053 | last_chars2 = chars2 | |
|
1054 | ||
|
1055 | if len(diffs) != x and diffs[x][0] == self.DIFF_DELETE: | |
|
1056 | # The location was deleted. | |
|
1057 | return last_chars2 | |
|
1058 | # Add the remaining len(character). | |
|
1059 | return last_chars2 + (loc - last_chars1) | |
|
1060 | ||
|
1061 | def diff_prettyHtml(self, diffs): | |
|
1062 | """Convert a diff array into a pretty HTML report. | |
|
1063 | ||
|
1064 | Args: | |
|
1065 | diffs: Array of diff tuples. | |
|
1066 | ||
|
1067 | Returns: | |
|
1068 | HTML representation. | |
|
1069 | """ | |
|
1070 | html = [] | |
|
1071 | for (op, data) in diffs: | |
|
1072 | text = (data.replace("&", "&").replace("<", "<") | |
|
1073 | .replace(">", ">").replace("\n", "¶<br>")) | |
|
1074 | if op == self.DIFF_INSERT: | |
|
1075 | html.append("<ins style=\"background:#e6ffe6;\">%s</ins>" % text) | |
|
1076 | elif op == self.DIFF_DELETE: | |
|
1077 | html.append("<del style=\"background:#ffe6e6;\">%s</del>" % text) | |
|
1078 | elif op == self.DIFF_EQUAL: | |
|
1079 | html.append("<span>%s</span>" % text) | |
|
1080 | return "".join(html) | |
|
1081 | ||
|
1082 | def diff_text1(self, diffs): | |
|
1083 | """Compute and return the source text (all equalities and deletions). | |
|
1084 | ||
|
1085 | Args: | |
|
1086 | diffs: Array of diff tuples. | |
|
1087 | ||
|
1088 | Returns: | |
|
1089 | Source text. | |
|
1090 | """ | |
|
1091 | text = [] | |
|
1092 | for (op, data) in diffs: | |
|
1093 | if op != self.DIFF_INSERT: | |
|
1094 | text.append(data) | |
|
1095 | return "".join(text) | |
|
1096 | ||
|
1097 | def diff_text2(self, diffs): | |
|
1098 | """Compute and return the destination text (all equalities and insertions). | |
|
1099 | ||
|
1100 | Args: | |
|
1101 | diffs: Array of diff tuples. | |
|
1102 | ||
|
1103 | Returns: | |
|
1104 | Destination text. | |
|
1105 | """ | |
|
1106 | text = [] | |
|
1107 | for (op, data) in diffs: | |
|
1108 | if op != self.DIFF_DELETE: | |
|
1109 | text.append(data) | |
|
1110 | return "".join(text) | |
|
1111 | ||
|
1112 | def diff_levenshtein(self, diffs): | |
|
1113 | """Compute the Levenshtein distance; the number of inserted, deleted or | |
|
1114 | substituted characters. | |
|
1115 | ||
|
1116 | Args: | |
|
1117 | diffs: Array of diff tuples. | |
|
1118 | ||
|
1119 | Returns: | |
|
1120 | Number of changes. | |
|
1121 | """ | |
|
1122 | levenshtein = 0 | |
|
1123 | insertions = 0 | |
|
1124 | deletions = 0 | |
|
1125 | for (op, data) in diffs: | |
|
1126 | if op == self.DIFF_INSERT: | |
|
1127 | insertions += len(data) | |
|
1128 | elif op == self.DIFF_DELETE: | |
|
1129 | deletions += len(data) | |
|
1130 | elif op == self.DIFF_EQUAL: | |
|
1131 | # A deletion and an insertion is one substitution. | |
|
1132 | levenshtein += max(insertions, deletions) | |
|
1133 | insertions = 0 | |
|
1134 | deletions = 0 | |
|
1135 | levenshtein += max(insertions, deletions) | |
|
1136 | return levenshtein | |
|
1137 | ||
|
1138 | def diff_toDelta(self, diffs): | |
|
1139 | """Crush the diff into an encoded string which describes the operations | |
|
1140 | required to transform text1 into text2. | |
|
1141 | E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. | |
|
1142 | Operations are tab-separated. Inserted text is escaped using %xx notation. | |
|
1143 | ||
|
1144 | Args: | |
|
1145 | diffs: Array of diff tuples. | |
|
1146 | ||
|
1147 | Returns: | |
|
1148 | Delta text. | |
|
1149 | """ | |
|
1150 | text = [] | |
|
1151 | for (op, data) in diffs: | |
|
1152 | if op == self.DIFF_INSERT: | |
|
1153 | # High ascii will raise UnicodeDecodeError. Use Unicode instead. | |
|
1154 | data = data.encode("utf-8") | |
|
1155 | text.append("+" + urllib.quote(data, "!~*'();/?:@&=+$,# ")) | |
|
1156 | elif op == self.DIFF_DELETE: | |
|
1157 | text.append("-%d" % len(data)) | |
|
1158 | elif op == self.DIFF_EQUAL: | |
|
1159 | text.append("=%d" % len(data)) | |
|
1160 | return "\t".join(text) | |
|
1161 | ||
|
1162 | def diff_fromDelta(self, text1, delta): | |
|
1163 | """Given the original text1, and an encoded string which describes the | |
|
1164 | operations required to transform text1 into text2, compute the full diff. | |
|
1165 | ||
|
1166 | Args: | |
|
1167 | text1: Source string for the diff. | |
|
1168 | delta: Delta text. | |
|
1169 | ||
|
1170 | Returns: | |
|
1171 | Array of diff tuples. | |
|
1172 | ||
|
1173 | Raises: | |
|
1174 | ValueError: If invalid input. | |
|
1175 | """ | |
|
1176 | if type(delta) == unicode: | |
|
1177 | # Deltas should be composed of a subset of ascii chars, Unicode not | |
|
1178 | # required. If this encode raises UnicodeEncodeError, delta is invalid. | |
|
1179 | delta = delta.encode("ascii") | |
|
1180 | diffs = [] | |
|
1181 | pointer = 0 # Cursor in text1 | |
|
1182 | tokens = delta.split("\t") | |
|
1183 | for token in tokens: | |
|
1184 | if token == "": | |
|
1185 | # Blank tokens are ok (from a trailing \t). | |
|
1186 | continue | |
|
1187 | # Each token begins with a one character parameter which specifies the | |
|
1188 | # operation of this token (delete, insert, equality). | |
|
1189 | param = token[1:] | |
|
1190 | if token[0] == "+": | |
|
1191 | param = urllib.unquote(param).decode("utf-8") | |
|
1192 | diffs.append((self.DIFF_INSERT, param)) | |
|
1193 | elif token[0] == "-" or token[0] == "=": | |
|
1194 | try: | |
|
1195 | n = int(param) | |
|
1196 | except ValueError: | |
|
1197 | raise ValueError("Invalid number in diff_fromDelta: " + param) | |
|
1198 | if n < 0: | |
|
1199 | raise ValueError("Negative number in diff_fromDelta: " + param) | |
|
1200 | text = text1[pointer : pointer + n] | |
|
1201 | pointer += n | |
|
1202 | if token[0] == "=": | |
|
1203 | diffs.append((self.DIFF_EQUAL, text)) | |
|
1204 | else: | |
|
1205 | diffs.append((self.DIFF_DELETE, text)) | |
|
1206 | else: | |
|
1207 | # Anything else is an error. | |
|
1208 | raise ValueError("Invalid diff operation in diff_fromDelta: " + | |
|
1209 | token[0]) | |
|
1210 | if pointer != len(text1): | |
|
1211 | raise ValueError( | |
|
1212 | "Delta length (%d) does not equal source text length (%d)." % | |
|
1213 | (pointer, len(text1))) | |
|
1214 | return diffs | |
|
1215 | ||
|
1216 | # MATCH FUNCTIONS | |
|
1217 | ||
|
1218 | def match_main(self, text, pattern, loc): | |
|
1219 | """Locate the best instance of 'pattern' in 'text' near 'loc'. | |
|
1220 | ||
|
1221 | Args: | |
|
1222 | text: The text to search. | |
|
1223 | pattern: The pattern to search for. | |
|
1224 | loc: The location to search around. | |
|
1225 | ||
|
1226 | Returns: | |
|
1227 | Best match index or -1. | |
|
1228 | """ | |
|
1229 | # Check for null inputs. | |
|
1230 | if text == None or pattern == None: | |
|
1231 | raise ValueError("Null inputs. (match_main)") | |
|
1232 | ||
|
1233 | loc = max(0, min(loc, len(text))) | |
|
1234 | if text == pattern: | |
|
1235 | # Shortcut (potentially not guaranteed by the algorithm) | |
|
1236 | return 0 | |
|
1237 | elif not text: | |
|
1238 | # Nothing to match. | |
|
1239 | return -1 | |
|
1240 | elif text[loc:loc + len(pattern)] == pattern: | |
|
1241 | # Perfect match at the perfect spot! (Includes case of null pattern) | |
|
1242 | return loc | |
|
1243 | else: | |
|
1244 | # Do a fuzzy compare. | |
|
1245 | match = self.match_bitap(text, pattern, loc) | |
|
1246 | return match | |
|
1247 | ||
|
1248 | def match_bitap(self, text, pattern, loc): | |
|
1249 | """Locate the best instance of 'pattern' in 'text' near 'loc' using the | |
|
1250 | Bitap algorithm. | |
|
1251 | ||
|
1252 | Args: | |
|
1253 | text: The text to search. | |
|
1254 | pattern: The pattern to search for. | |
|
1255 | loc: The location to search around. | |
|
1256 | ||
|
1257 | Returns: | |
|
1258 | Best match index or -1. | |
|
1259 | """ | |
|
1260 | # Python doesn't have a maxint limit, so ignore this check. | |
|
1261 | #if self.Match_MaxBits != 0 and len(pattern) > self.Match_MaxBits: | |
|
1262 | # raise ValueError("Pattern too long for this application.") | |
|
1263 | ||
|
1264 | # Initialise the alphabet. | |
|
1265 | s = self.match_alphabet(pattern) | |
|
1266 | ||
|
1267 | def match_bitapScore(e, x): | |
|
1268 | """Compute and return the score for a match with e errors and x location. | |
|
1269 | Accesses loc and pattern through being a closure. | |
|
1270 | ||
|
1271 | Args: | |
|
1272 | e: Number of errors in match. | |
|
1273 | x: Location of match. | |
|
1274 | ||
|
1275 | Returns: | |
|
1276 | Overall score for match (0.0 = good, 1.0 = bad). | |
|
1277 | """ | |
|
1278 | accuracy = float(e) / len(pattern) | |
|
1279 | proximity = abs(loc - x) | |
|
1280 | if not self.Match_Distance: | |
|
1281 | # Dodge divide by zero error. | |
|
1282 | return proximity and 1.0 or accuracy | |
|
1283 | return accuracy + (proximity / float(self.Match_Distance)) | |
|
1284 | ||
|
1285 | # Highest score beyond which we give up. | |
|
1286 | score_threshold = self.Match_Threshold | |
|
1287 | # Is there a nearby exact match? (speedup) | |
|
1288 | best_loc = text.find(pattern, loc) | |
|
1289 | if best_loc != -1: | |
|
1290 | score_threshold = min(match_bitapScore(0, best_loc), score_threshold) | |
|
1291 | # What about in the other direction? (speedup) | |
|
1292 | best_loc = text.rfind(pattern, loc + len(pattern)) | |
|
1293 | if best_loc != -1: | |
|
1294 | score_threshold = min(match_bitapScore(0, best_loc), score_threshold) | |
|
1295 | ||
|
1296 | # Initialise the bit arrays. | |
|
1297 | matchmask = 1 << (len(pattern) - 1) | |
|
1298 | best_loc = -1 | |
|
1299 | ||
|
1300 | bin_max = len(pattern) + len(text) | |
|
1301 | # Empty initialization added to appease pychecker. | |
|
1302 | last_rd = None | |
|
1303 | for d in xrange(len(pattern)): | |
|
1304 | # Scan for the best match each iteration allows for one more error. | |
|
1305 | # Run a binary search to determine how far from 'loc' we can stray at | |
|
1306 | # this error level. | |
|
1307 | bin_min = 0 | |
|
1308 | bin_mid = bin_max | |
|
1309 | while bin_min < bin_mid: | |
|
1310 | if match_bitapScore(d, loc + bin_mid) <= score_threshold: | |
|
1311 | bin_min = bin_mid | |
|
1312 | else: | |
|
1313 | bin_max = bin_mid | |
|
1314 | bin_mid = (bin_max - bin_min) // 2 + bin_min | |
|
1315 | ||
|
1316 | # Use the result from this iteration as the maximum for the next. | |
|
1317 | bin_max = bin_mid | |
|
1318 | start = max(1, loc - bin_mid + 1) | |
|
1319 | finish = min(loc + bin_mid, len(text)) + len(pattern) | |
|
1320 | ||
|
1321 | rd = [0] * (finish + 2) | |
|
1322 | rd[finish + 1] = (1 << d) - 1 | |
|
1323 | for j in xrange(finish, start - 1, -1): | |
|
1324 | if len(text) <= j - 1: | |
|
1325 | # Out of range. | |
|
1326 | charMatch = 0 | |
|
1327 | else: | |
|
1328 | charMatch = s.get(text[j - 1], 0) | |
|
1329 | if d == 0: # First pass: exact match. | |
|
1330 | rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | |
|
1331 | else: # Subsequent passes: fuzzy match. | |
|
1332 | rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | ( | |
|
1333 | ((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1] | |
|
1334 | if rd[j] & matchmask: | |
|
1335 | score = match_bitapScore(d, j - 1) | |
|
1336 | # This match will almost certainly be better than any existing match. | |
|
1337 | # But check anyway. | |
|
1338 | if score <= score_threshold: | |
|
1339 | # Told you so. | |
|
1340 | score_threshold = score | |
|
1341 | best_loc = j - 1 | |
|
1342 | if best_loc > loc: | |
|
1343 | # When passing loc, don't exceed our current distance from loc. | |
|
1344 | start = max(1, 2 * loc - best_loc) | |
|
1345 | else: | |
|
1346 | # Already passed loc, downhill from here on in. | |
|
1347 | break | |
|
1348 | # No hope for a (better) match at greater error levels. | |
|
1349 | if match_bitapScore(d + 1, loc) > score_threshold: | |
|
1350 | break | |
|
1351 | last_rd = rd | |
|
1352 | return best_loc | |
|
1353 | ||
|
1354 | def match_alphabet(self, pattern): | |
|
1355 | """Initialise the alphabet for the Bitap algorithm. | |
|
1356 | ||
|
1357 | Args: | |
|
1358 | pattern: The text to encode. | |
|
1359 | ||
|
1360 | Returns: | |
|
1361 | Hash of character locations. | |
|
1362 | """ | |
|
1363 | s = {} | |
|
1364 | for char in pattern: | |
|
1365 | s[char] = 0 | |
|
1366 | for i in xrange(len(pattern)): | |
|
1367 | s[pattern[i]] |= 1 << (len(pattern) - i - 1) | |
|
1368 | return s | |
|
1369 | ||
|
1370 | # PATCH FUNCTIONS | |
|
1371 | ||
|
1372 | def patch_addContext(self, patch, text): | |
|
1373 | """Increase the context until it is unique, | |
|
1374 | but don't let the pattern expand beyond Match_MaxBits. | |
|
1375 | ||
|
1376 | Args: | |
|
1377 | patch: The patch to grow. | |
|
1378 | text: Source text. | |
|
1379 | """ | |
|
1380 | if len(text) == 0: | |
|
1381 | return | |
|
1382 | pattern = text[patch.start2 : patch.start2 + patch.length1] | |
|
1383 | padding = 0 | |
|
1384 | ||
|
1385 | # Look for the first and last matches of pattern in text. If two different | |
|
1386 | # matches are found, increase the pattern length. | |
|
1387 | while (text.find(pattern) != text.rfind(pattern) and (self.Match_MaxBits == | |
|
1388 | 0 or len(pattern) < self.Match_MaxBits - self.Patch_Margin - | |
|
1389 | self.Patch_Margin)): | |
|
1390 | padding += self.Patch_Margin | |
|
1391 | pattern = text[max(0, patch.start2 - padding) : | |
|
1392 | patch.start2 + patch.length1 + padding] | |
|
1393 | # Add one chunk for good luck. | |
|
1394 | padding += self.Patch_Margin | |
|
1395 | ||
|
1396 | # Add the prefix. | |
|
1397 | prefix = text[max(0, patch.start2 - padding) : patch.start2] | |
|
1398 | if prefix: | |
|
1399 | patch.diffs[:0] = [(self.DIFF_EQUAL, prefix)] | |
|
1400 | # Add the suffix. | |
|
1401 | suffix = text[patch.start2 + patch.length1 : | |
|
1402 | patch.start2 + patch.length1 + padding] | |
|
1403 | if suffix: | |
|
1404 | patch.diffs.append((self.DIFF_EQUAL, suffix)) | |
|
1405 | ||
|
1406 | # Roll back the start points. | |
|
1407 | patch.start1 -= len(prefix) | |
|
1408 | patch.start2 -= len(prefix) | |
|
1409 | # Extend lengths. | |
|
1410 | patch.length1 += len(prefix) + len(suffix) | |
|
1411 | patch.length2 += len(prefix) + len(suffix) | |
|
1412 | ||
|
1413 | def patch_make(self, a, b=None, c=None): | |
|
1414 | """Compute a list of patches to turn text1 into text2. | |
|
1415 | Use diffs if provided, otherwise compute it ourselves. | |
|
1416 | There are four ways to call this function, depending on what data is | |
|
1417 | available to the caller: | |
|
1418 | Method 1: | |
|
1419 | a = text1, b = text2 | |
|
1420 | Method 2: | |
|
1421 | a = diffs | |
|
1422 | Method 3 (optimal): | |
|
1423 | a = text1, b = diffs | |
|
1424 | Method 4 (deprecated, use method 3): | |
|
1425 | a = text1, b = text2, c = diffs | |
|
1426 | ||
|
1427 | Args: | |
|
1428 | a: text1 (methods 1,3,4) or Array of diff tuples for text1 to | |
|
1429 | text2 (method 2). | |
|
1430 | b: text2 (methods 1,4) or Array of diff tuples for text1 to | |
|
1431 | text2 (method 3) or undefined (method 2). | |
|
1432 | c: Array of diff tuples for text1 to text2 (method 4) or | |
|
1433 | undefined (methods 1,2,3). | |
|
1434 | ||
|
1435 | Returns: | |
|
1436 | Array of Patch objects. | |
|
1437 | """ | |
|
1438 | text1 = None | |
|
1439 | diffs = None | |
|
1440 | # Note that texts may arrive as 'str' or 'unicode'. | |
|
1441 | if isinstance(a, basestring) and isinstance(b, basestring) and c is None: | |
|
1442 | # Method 1: text1, text2 | |
|
1443 | # Compute diffs from text1 and text2. | |
|
1444 | text1 = a | |
|
1445 | diffs = self.diff_main(text1, b, True) | |
|
1446 | if len(diffs) > 2: | |
|
1447 | self.diff_cleanupSemantic(diffs) | |
|
1448 | self.diff_cleanupEfficiency(diffs) | |
|
1449 | elif isinstance(a, list) and b is None and c is None: | |
|
1450 | # Method 2: diffs | |
|
1451 | # Compute text1 from diffs. | |
|
1452 | diffs = a | |
|
1453 | text1 = self.diff_text1(diffs) | |
|
1454 | elif isinstance(a, basestring) and isinstance(b, list) and c is None: | |
|
1455 | # Method 3: text1, diffs | |
|
1456 | text1 = a | |
|
1457 | diffs = b | |
|
1458 | elif (isinstance(a, basestring) and isinstance(b, basestring) and | |
|
1459 | isinstance(c, list)): | |
|
1460 | # Method 4: text1, text2, diffs | |
|
1461 | # text2 is not used. | |
|
1462 | text1 = a | |
|
1463 | diffs = c | |
|
1464 | else: | |
|
1465 | raise ValueError("Unknown call format to patch_make.") | |
|
1466 | ||
|
1467 | if not diffs: | |
|
1468 | return [] # Get rid of the None case. | |
|
1469 | patches = [] | |
|
1470 | patch = patch_obj() | |
|
1471 | char_count1 = 0 # Number of characters into the text1 string. | |
|
1472 | char_count2 = 0 # Number of characters into the text2 string. | |
|
1473 | prepatch_text = text1 # Recreate the patches to determine context info. | |
|
1474 | postpatch_text = text1 | |
|
1475 | for x in xrange(len(diffs)): | |
|
1476 | (diff_type, diff_text) = diffs[x] | |
|
1477 | if len(patch.diffs) == 0 and diff_type != self.DIFF_EQUAL: | |
|
1478 | # A new patch starts here. | |
|
1479 | patch.start1 = char_count1 | |
|
1480 | patch.start2 = char_count2 | |
|
1481 | if diff_type == self.DIFF_INSERT: | |
|
1482 | # Insertion | |
|
1483 | patch.diffs.append(diffs[x]) | |
|
1484 | patch.length2 += len(diff_text) | |
|
1485 | postpatch_text = (postpatch_text[:char_count2] + diff_text + | |
|
1486 | postpatch_text[char_count2:]) | |
|
1487 | elif diff_type == self.DIFF_DELETE: | |
|
1488 | # Deletion. | |
|
1489 | patch.length1 += len(diff_text) | |
|
1490 | patch.diffs.append(diffs[x]) | |
|
1491 | postpatch_text = (postpatch_text[:char_count2] + | |
|
1492 | postpatch_text[char_count2 + len(diff_text):]) | |
|
1493 | elif (diff_type == self.DIFF_EQUAL and | |
|
1494 | len(diff_text) <= 2 * self.Patch_Margin and | |
|
1495 | len(patch.diffs) != 0 and len(diffs) != x + 1): | |
|
1496 | # Small equality inside a patch. | |
|
1497 | patch.diffs.append(diffs[x]) | |
|
1498 | patch.length1 += len(diff_text) | |
|
1499 | patch.length2 += len(diff_text) | |
|
1500 | ||
|
1501 | if (diff_type == self.DIFF_EQUAL and | |
|
1502 | len(diff_text) >= 2 * self.Patch_Margin): | |
|
1503 | # Time for a new patch. | |
|
1504 | if len(patch.diffs) != 0: | |
|
1505 | self.patch_addContext(patch, prepatch_text) | |
|
1506 | patches.append(patch) | |
|
1507 | patch = patch_obj() | |
|
1508 | # Unlike Unidiff, our patch lists have a rolling context. | |
|
1509 | # http://code.google.com/p/google-diff-match-patch/wiki/Unidiff | |
|
1510 | # Update prepatch text & pos to reflect the application of the | |
|
1511 | # just completed patch. | |
|
1512 | prepatch_text = postpatch_text | |
|
1513 | char_count1 = char_count2 | |
|
1514 | ||
|
1515 | # Update the current character count. | |
|
1516 | if diff_type != self.DIFF_INSERT: | |
|
1517 | char_count1 += len(diff_text) | |
|
1518 | if diff_type != self.DIFF_DELETE: | |
|
1519 | char_count2 += len(diff_text) | |
|
1520 | ||
|
1521 | # Pick up the leftover patch if not empty. | |
|
1522 | if len(patch.diffs) != 0: | |
|
1523 | self.patch_addContext(patch, prepatch_text) | |
|
1524 | patches.append(patch) | |
|
1525 | return patches | |
|
1526 | ||
|
1527 | def patch_deepCopy(self, patches): | |
|
1528 | """Given an array of patches, return another array that is identical. | |
|
1529 | ||
|
1530 | Args: | |
|
1531 | patches: Array of Patch objects. | |
|
1532 | ||
|
1533 | Returns: | |
|
1534 | Array of Patch objects. | |
|
1535 | """ | |
|
1536 | patchesCopy = [] | |
|
1537 | for patch in patches: | |
|
1538 | patchCopy = patch_obj() | |
|
1539 | # No need to deep copy the tuples since they are immutable. | |
|
1540 | patchCopy.diffs = patch.diffs[:] | |
|
1541 | patchCopy.start1 = patch.start1 | |
|
1542 | patchCopy.start2 = patch.start2 | |
|
1543 | patchCopy.length1 = patch.length1 | |
|
1544 | patchCopy.length2 = patch.length2 | |
|
1545 | patchesCopy.append(patchCopy) | |
|
1546 | return patchesCopy | |
|
1547 | ||
|
1548 | def patch_apply(self, patches, text): | |
|
1549 | """Merge a set of patches onto the text. Return a patched text, as well | |
|
1550 | as a list of true/false values indicating which patches were applied. | |
|
1551 | ||
|
1552 | Args: | |
|
1553 | patches: Array of Patch objects. | |
|
1554 | text: Old text. | |
|
1555 | ||
|
1556 | Returns: | |
|
1557 | Two element Array, containing the new text and an array of boolean values. | |
|
1558 | """ | |
|
1559 | if not patches: | |
|
1560 | return (text, []) | |
|
1561 | ||
|
1562 | # Deep copy the patches so that no changes are made to originals. | |
|
1563 | patches = self.patch_deepCopy(patches) | |
|
1564 | ||
|
1565 | nullPadding = self.patch_addPadding(patches) | |
|
1566 | text = nullPadding + text + nullPadding | |
|
1567 | self.patch_splitMax(patches) | |
|
1568 | ||
|
1569 | # delta keeps track of the offset between the expected and actual location | |
|
1570 | # of the previous patch. If there are patches expected at positions 10 and | |
|
1571 | # 20, but the first patch was found at 12, delta is 2 and the second patch | |
|
1572 | # has an effective expected position of 22. | |
|
1573 | delta = 0 | |
|
1574 | results = [] | |
|
1575 | for patch in patches: | |
|
1576 | expected_loc = patch.start2 + delta | |
|
1577 | text1 = self.diff_text1(patch.diffs) | |
|
1578 | end_loc = -1 | |
|
1579 | if len(text1) > self.Match_MaxBits: | |
|
1580 | # patch_splitMax will only provide an oversized pattern in the case of | |
|
1581 | # a monster delete. | |
|
1582 | start_loc = self.match_main(text, text1[:self.Match_MaxBits], | |
|
1583 | expected_loc) | |
|
1584 | if start_loc != -1: | |
|
1585 | end_loc = self.match_main(text, text1[-self.Match_MaxBits:], | |
|
1586 | expected_loc + len(text1) - self.Match_MaxBits) | |
|
1587 | if end_loc == -1 or start_loc >= end_loc: | |
|
1588 | # Can't find valid trailing context. Drop this patch. | |
|
1589 | start_loc = -1 | |
|
1590 | else: | |
|
1591 | start_loc = self.match_main(text, text1, expected_loc) | |
|
1592 | if start_loc == -1: | |
|
1593 | # No match found. :( | |
|
1594 | results.append(False) | |
|
1595 | # Subtract the delta for this failed patch from subsequent patches. | |
|
1596 | delta -= patch.length2 - patch.length1 | |
|
1597 | else: | |
|
1598 | # Found a match. :) | |
|
1599 | results.append(True) | |
|
1600 | delta = start_loc - expected_loc | |
|
1601 | if end_loc == -1: | |
|
1602 | text2 = text[start_loc : start_loc + len(text1)] | |
|
1603 | else: | |
|
1604 | text2 = text[start_loc : end_loc + self.Match_MaxBits] | |
|
1605 | if text1 == text2: | |
|
1606 | # Perfect match, just shove the replacement text in. | |
|
1607 | text = (text[:start_loc] + self.diff_text2(patch.diffs) + | |
|
1608 | text[start_loc + len(text1):]) | |
|
1609 | else: | |
|
1610 | # Imperfect match. | |
|
1611 | # Run a diff to get a framework of equivalent indices. | |
|
1612 | diffs = self.diff_main(text1, text2, False) | |
|
1613 | if (len(text1) > self.Match_MaxBits and | |
|
1614 | self.diff_levenshtein(diffs) / float(len(text1)) > | |
|
1615 | self.Patch_DeleteThreshold): | |
|
1616 | # The end points match, but the content is unacceptably bad. | |
|
1617 | results[-1] = False | |
|
1618 | else: | |
|
1619 | self.diff_cleanupSemanticLossless(diffs) | |
|
1620 | index1 = 0 | |
|
1621 | for (op, data) in patch.diffs: | |
|
1622 | if op != self.DIFF_EQUAL: | |
|
1623 | index2 = self.diff_xIndex(diffs, index1) | |
|
1624 | if op == self.DIFF_INSERT: # Insertion | |
|
1625 | text = text[:start_loc + index2] + data + text[start_loc + | |
|
1626 | index2:] | |
|
1627 | elif op == self.DIFF_DELETE: # Deletion | |
|
1628 | text = text[:start_loc + index2] + text[start_loc + | |
|
1629 | self.diff_xIndex(diffs, index1 + len(data)):] | |
|
1630 | if op != self.DIFF_DELETE: | |
|
1631 | index1 += len(data) | |
|
1632 | # Strip the padding off. | |
|
1633 | text = text[len(nullPadding):-len(nullPadding)] | |
|
1634 | return (text, results) | |
|
1635 | ||
|
1636 | def patch_addPadding(self, patches): | |
|
1637 | """Add some padding on text start and end so that edges can match | |
|
1638 | something. Intended to be called only from within patch_apply. | |
|
1639 | ||
|
1640 | Args: | |
|
1641 | patches: Array of Patch objects. | |
|
1642 | ||
|
1643 | Returns: | |
|
1644 | The padding string added to each side. | |
|
1645 | """ | |
|
1646 | paddingLength = self.Patch_Margin | |
|
1647 | nullPadding = "" | |
|
1648 | for x in xrange(1, paddingLength + 1): | |
|
1649 | nullPadding += chr(x) | |
|
1650 | ||
|
1651 | # Bump all the patches forward. | |
|
1652 | for patch in patches: | |
|
1653 | patch.start1 += paddingLength | |
|
1654 | patch.start2 += paddingLength | |
|
1655 | ||
|
1656 | # Add some padding on start of first diff. | |
|
1657 | patch = patches[0] | |
|
1658 | diffs = patch.diffs | |
|
1659 | if not diffs or diffs[0][0] != self.DIFF_EQUAL: | |
|
1660 | # Add nullPadding equality. | |
|
1661 | diffs.insert(0, (self.DIFF_EQUAL, nullPadding)) | |
|
1662 | patch.start1 -= paddingLength # Should be 0. | |
|
1663 | patch.start2 -= paddingLength # Should be 0. | |
|
1664 | patch.length1 += paddingLength | |
|
1665 | patch.length2 += paddingLength | |
|
1666 | elif paddingLength > len(diffs[0][1]): | |
|
1667 | # Grow first equality. | |
|
1668 | extraLength = paddingLength - len(diffs[0][1]) | |
|
1669 | newText = nullPadding[len(diffs[0][1]):] + diffs[0][1] | |
|
1670 | diffs[0] = (diffs[0][0], newText) | |
|
1671 | patch.start1 -= extraLength | |
|
1672 | patch.start2 -= extraLength | |
|
1673 | patch.length1 += extraLength | |
|
1674 | patch.length2 += extraLength | |
|
1675 | ||
|
1676 | # Add some padding on end of last diff. | |
|
1677 | patch = patches[-1] | |
|
1678 | diffs = patch.diffs | |
|
1679 | if not diffs or diffs[-1][0] != self.DIFF_EQUAL: | |
|
1680 | # Add nullPadding equality. | |
|
1681 | diffs.append((self.DIFF_EQUAL, nullPadding)) | |
|
1682 | patch.length1 += paddingLength | |
|
1683 | patch.length2 += paddingLength | |
|
1684 | elif paddingLength > len(diffs[-1][1]): | |
|
1685 | # Grow last equality. | |
|
1686 | extraLength = paddingLength - len(diffs[-1][1]) | |
|
1687 | newText = diffs[-1][1] + nullPadding[:extraLength] | |
|
1688 | diffs[-1] = (diffs[-1][0], newText) | |
|
1689 | patch.length1 += extraLength | |
|
1690 | patch.length2 += extraLength | |
|
1691 | ||
|
1692 | return nullPadding | |
|
1693 | ||
|
1694 | def patch_splitMax(self, patches): | |
|
1695 | """Look through the patches and break up any which are longer than the | |
|
1696 | maximum limit of the match algorithm. | |
|
1697 | Intended to be called only from within patch_apply. | |
|
1698 | ||
|
1699 | Args: | |
|
1700 | patches: Array of Patch objects. | |
|
1701 | """ | |
|
1702 | patch_size = self.Match_MaxBits | |
|
1703 | if patch_size == 0: | |
|
1704 | # Python has the option of not splitting strings due to its ability | |
|
1705 | # to handle integers of arbitrary precision. | |
|
1706 | return | |
|
1707 | for x in xrange(len(patches)): | |
|
1708 | if patches[x].length1 <= patch_size: | |
|
1709 | continue | |
|
1710 | bigpatch = patches[x] | |
|
1711 | # Remove the big old patch. | |
|
1712 | del patches[x] | |
|
1713 | x -= 1 | |
|
1714 | start1 = bigpatch.start1 | |
|
1715 | start2 = bigpatch.start2 | |
|
1716 | precontext = '' | |
|
1717 | while len(bigpatch.diffs) != 0: | |
|
1718 | # Create one of several smaller patches. | |
|
1719 | patch = patch_obj() | |
|
1720 | empty = True | |
|
1721 | patch.start1 = start1 - len(precontext) | |
|
1722 | patch.start2 = start2 - len(precontext) | |
|
1723 | if precontext: | |
|
1724 | patch.length1 = patch.length2 = len(precontext) | |
|
1725 | patch.diffs.append((self.DIFF_EQUAL, precontext)) | |
|
1726 | ||
|
1727 | while (len(bigpatch.diffs) != 0 and | |
|
1728 | patch.length1 < patch_size - self.Patch_Margin): | |
|
1729 | (diff_type, diff_text) = bigpatch.diffs[0] | |
|
1730 | if diff_type == self.DIFF_INSERT: | |
|
1731 | # Insertions are harmless. | |
|
1732 | patch.length2 += len(diff_text) | |
|
1733 | start2 += len(diff_text) | |
|
1734 | patch.diffs.append(bigpatch.diffs.pop(0)) | |
|
1735 | empty = False | |
|
1736 | elif (diff_type == self.DIFF_DELETE and len(patch.diffs) == 1 and | |
|
1737 | patch.diffs[0][0] == self.DIFF_EQUAL and | |
|
1738 | len(diff_text) > 2 * patch_size): | |
|
1739 | # This is a large deletion. Let it pass in one chunk. | |
|
1740 | patch.length1 += len(diff_text) | |
|
1741 | start1 += len(diff_text) | |
|
1742 | empty = False | |
|
1743 | patch.diffs.append((diff_type, diff_text)) | |
|
1744 | del bigpatch.diffs[0] | |
|
1745 | else: | |
|
1746 | # Deletion or equality. Only take as much as we can stomach. | |
|
1747 | diff_text = diff_text[:patch_size - patch.length1 - | |
|
1748 | self.Patch_Margin] | |
|
1749 | patch.length1 += len(diff_text) | |
|
1750 | start1 += len(diff_text) | |
|
1751 | if diff_type == self.DIFF_EQUAL: | |
|
1752 | patch.length2 += len(diff_text) | |
|
1753 | start2 += len(diff_text) | |
|
1754 | else: | |
|
1755 | empty = False | |
|
1756 | ||
|
1757 | patch.diffs.append((diff_type, diff_text)) | |
|
1758 | if diff_text == bigpatch.diffs[0][1]: | |
|
1759 | del bigpatch.diffs[0] | |
|
1760 | else: | |
|
1761 | bigpatch.diffs[0] = (bigpatch.diffs[0][0], | |
|
1762 | bigpatch.diffs[0][1][len(diff_text):]) | |
|
1763 | ||
|
1764 | # Compute the head context for the next patch. | |
|
1765 | precontext = self.diff_text2(patch.diffs) | |
|
1766 | precontext = precontext[-self.Patch_Margin:] | |
|
1767 | # Append the end context for this patch. | |
|
1768 | postcontext = self.diff_text1(bigpatch.diffs)[:self.Patch_Margin] | |
|
1769 | if postcontext: | |
|
1770 | patch.length1 += len(postcontext) | |
|
1771 | patch.length2 += len(postcontext) | |
|
1772 | if len(patch.diffs) != 0 and patch.diffs[-1][0] == self.DIFF_EQUAL: | |
|
1773 | patch.diffs[-1] = (self.DIFF_EQUAL, patch.diffs[-1][1] + | |
|
1774 | postcontext) | |
|
1775 | else: | |
|
1776 | patch.diffs.append((self.DIFF_EQUAL, postcontext)) | |
|
1777 | ||
|
1778 | if not empty: | |
|
1779 | x += 1 | |
|
1780 | patches.insert(x, patch) | |
|
1781 | ||
|
1782 | def patch_toText(self, patches): | |
|
1783 | """Take a list of patches and return a textual representation. | |
|
1784 | ||
|
1785 | Args: | |
|
1786 | patches: Array of Patch objects. | |
|
1787 | ||
|
1788 | Returns: | |
|
1789 | Text representation of patches. | |
|
1790 | """ | |
|
1791 | text = [] | |
|
1792 | for patch in patches: | |
|
1793 | text.append(str(patch)) | |
|
1794 | return "".join(text) | |
|
1795 | ||
|
1796 | def patch_fromText(self, textline): | |
|
1797 | """Parse a textual representation of patches and return a list of patch | |
|
1798 | objects. | |
|
1799 | ||
|
1800 | Args: | |
|
1801 | textline: Text representation of patches. | |
|
1802 | ||
|
1803 | Returns: | |
|
1804 | Array of Patch objects. | |
|
1805 | ||
|
1806 | Raises: | |
|
1807 | ValueError: If invalid input. | |
|
1808 | """ | |
|
1809 | if type(textline) == unicode: | |
|
1810 | # Patches should be composed of a subset of ascii chars, Unicode not | |
|
1811 | # required. If this encode raises UnicodeEncodeError, patch is invalid. | |
|
1812 | textline = textline.encode("ascii") | |
|
1813 | patches = [] | |
|
1814 | if not textline: | |
|
1815 | return patches | |
|
1816 | text = textline.split('\n') | |
|
1817 | while len(text) != 0: | |
|
1818 | m = re.match("^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$", text[0]) | |
|
1819 | if not m: | |
|
1820 | raise ValueError("Invalid patch string: " + text[0]) | |
|
1821 | patch = patch_obj() | |
|
1822 | patches.append(patch) | |
|
1823 | patch.start1 = int(m.group(1)) | |
|
1824 | if m.group(2) == '': | |
|
1825 | patch.start1 -= 1 | |
|
1826 | patch.length1 = 1 | |
|
1827 | elif m.group(2) == '0': | |
|
1828 | patch.length1 = 0 | |
|
1829 | else: | |
|
1830 | patch.start1 -= 1 | |
|
1831 | patch.length1 = int(m.group(2)) | |
|
1832 | ||
|
1833 | patch.start2 = int(m.group(3)) | |
|
1834 | if m.group(4) == '': | |
|
1835 | patch.start2 -= 1 | |
|
1836 | patch.length2 = 1 | |
|
1837 | elif m.group(4) == '0': | |
|
1838 | patch.length2 = 0 | |
|
1839 | else: | |
|
1840 | patch.start2 -= 1 | |
|
1841 | patch.length2 = int(m.group(4)) | |
|
1842 | ||
|
1843 | del text[0] | |
|
1844 | ||
|
1845 | while len(text) != 0: | |
|
1846 | if text[0]: | |
|
1847 | sign = text[0][0] | |
|
1848 | else: | |
|
1849 | sign = '' | |
|
1850 | line = urllib.unquote(text[0][1:]) | |
|
1851 | line = line.decode("utf-8") | |
|
1852 | if sign == '+': | |
|
1853 | # Insertion. | |
|
1854 | patch.diffs.append((self.DIFF_INSERT, line)) | |
|
1855 | elif sign == '-': | |
|
1856 | # Deletion. | |
|
1857 | patch.diffs.append((self.DIFF_DELETE, line)) | |
|
1858 | elif sign == ' ': | |
|
1859 | # Minor equality. | |
|
1860 | patch.diffs.append((self.DIFF_EQUAL, line)) | |
|
1861 | elif sign == '@': | |
|
1862 | # Start of next patch. | |
|
1863 | break | |
|
1864 | elif sign == '': | |
|
1865 | # Blank line? Whatever. | |
|
1866 | pass | |
|
1867 | else: | |
|
1868 | # WTF? | |
|
1869 | raise ValueError("Invalid patch mode: '%s'\n%s" % (sign, line)) | |
|
1870 | del text[0] | |
|
1871 | return patches | |
|
1872 | ||
|
1873 | ||
|
1874 | class patch_obj: | |
|
1875 | """Class representing one patch operation. | |
|
1876 | """ | |
|
1877 | ||
|
1878 | def __init__(self): | |
|
1879 | """Initializes with an empty list of diffs. | |
|
1880 | """ | |
|
1881 | self.diffs = [] | |
|
1882 | self.start1 = None | |
|
1883 | self.start2 = None | |
|
1884 | self.length1 = 0 | |
|
1885 | self.length2 = 0 | |
|
1886 | ||
|
1887 | def __str__(self): | |
|
1888 | """Emmulate GNU diff's format. | |
|
1889 | Header: @@ -382,8 +481,9 @@ | |
|
1890 | Indicies are printed as 1-based, not 0-based. | |
|
1891 | ||
|
1892 | Returns: | |
|
1893 | The GNU diff string. | |
|
1894 | """ | |
|
1895 | if self.length1 == 0: | |
|
1896 | coords1 = str(self.start1) + ",0" | |
|
1897 | elif self.length1 == 1: | |
|
1898 | coords1 = str(self.start1 + 1) | |
|
1899 | else: | |
|
1900 | coords1 = str(self.start1 + 1) + "," + str(self.length1) | |
|
1901 | if self.length2 == 0: | |
|
1902 | coords2 = str(self.start2) + ",0" | |
|
1903 | elif self.length2 == 1: | |
|
1904 | coords2 = str(self.start2 + 1) | |
|
1905 | else: | |
|
1906 | coords2 = str(self.start2 + 1) + "," + str(self.length2) | |
|
1907 | text = ["@@ -", coords1, " +", coords2, " @@\n"] | |
|
1908 | # Escape the body of the patch with %xx notation. | |
|
1909 | for (op, data) in self.diffs: | |
|
1910 | if op == diff_match_patch.DIFF_INSERT: | |
|
1911 | text.append("+") | |
|
1912 | elif op == diff_match_patch.DIFF_DELETE: | |
|
1913 | text.append("-") | |
|
1914 | elif op == diff_match_patch.DIFF_EQUAL: | |
|
1915 | text.append(" ") | |
|
1916 | # High ascii will raise UnicodeDecodeError. Use Unicode instead. | |
|
1917 | data = data.encode("utf-8") | |
|
1918 | text.append(urllib.quote(data, "!~*'();/?:@&=+$,# ") + "\n") | |
|
1919 | return "".join(text) No newline at end of file |
@@ -0,0 +1,398 b'' | |||
|
1 | <%def name="diff_line_anchor(filename, line, type)"><% | |
|
2 | return '%s_%s_%i' % (h.safeid(filename), type, line) | |
|
3 | %></%def> | |
|
4 | ||
|
5 | <%def name="action_class(action)"><% | |
|
6 | return { | |
|
7 | '-': 'cb-deletion', | |
|
8 | '+': 'cb-addition', | |
|
9 | ' ': 'cb-context', | |
|
10 | }.get(action, 'cb-empty') | |
|
11 | %></%def> | |
|
12 | ||
|
13 | <%def name="op_class(op_id)"><% | |
|
14 | return { | |
|
15 | DEL_FILENODE: 'deletion', # file deleted | |
|
16 | BIN_FILENODE: 'warning' # binary diff hidden | |
|
17 | }.get(op_id, 'addition') | |
|
18 | %></%def> | |
|
19 | ||
|
20 | <%def name="link_for(**kw)"><% | |
|
21 | new_args = request.GET.mixed() | |
|
22 | new_args.update(kw) | |
|
23 | return h.url('', **new_args) | |
|
24 | %></%def> | |
|
25 | ||
|
26 | <%def name="render_diffset(diffset, | |
|
27 | ||
|
28 | # collapse all file diff entries when there are more than this amount of files in the diff | |
|
29 | collapse_when_files_over=20, | |
|
30 | ||
|
31 | # collapse lines in the diff when more than this amount of lines changed in the file diff | |
|
32 | lines_changed_limit=500, | |
|
33 | )"> | |
|
34 | <% | |
|
35 | # TODO: dan: move this to an argument - and set a cookie so that it is saved | |
|
36 | # default option for future requests | |
|
37 | diff_mode = request.GET.get('diffmode', 'sideside') | |
|
38 | if diff_mode not in ('sideside', 'unified'): | |
|
39 | diff_mode = 'sideside' | |
|
40 | ||
|
41 | collapse_all = len(diffset.files) > collapse_when_files_over | |
|
42 | %> | |
|
43 | ||
|
44 | %if diff_mode == 'sideside': | |
|
45 | <style> | |
|
46 | .wrapper { | |
|
47 | max-width: 1600px !important; | |
|
48 | } | |
|
49 | </style> | |
|
50 | %endif | |
|
51 | ||
|
52 | % if diffset.limited_diff: | |
|
53 | <div class="alert alert-warning"> | |
|
54 | ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a> | |
|
55 | </div> | |
|
56 | % endif | |
|
57 | ||
|
58 | <div class="cs_files"> | |
|
59 | <div class="cs_files_title"> | |
|
60 | %if diffset.files: | |
|
61 | <div class="pull-right"> | |
|
62 | <div class="btn-group"> | |
|
63 | <a | |
|
64 | class="btn ${diff_mode == 'sideside' and 'btn-primary'} tooltip" | |
|
65 | title="${_('View side by side')}" | |
|
66 | href="${link_for(diffmode='sideside')}"> | |
|
67 | <span>${_('Side by Side')}</span> | |
|
68 | </a> | |
|
69 | <a | |
|
70 | class="btn ${diff_mode == 'unified' and 'btn-primary'} tooltip" | |
|
71 | title="${_('View unified')}" href="${link_for(diffmode='unified')}"> | |
|
72 | <span>${_('Unified')}</span> | |
|
73 | </a> | |
|
74 | </div> | |
|
75 | </div> | |
|
76 | <div class="pull-left"> | |
|
77 | <div class="btn-group"> | |
|
78 | <a | |
|
79 | class="btn" | |
|
80 | href="#" | |
|
81 | onclick="$('input[class=diff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a> | |
|
82 | <a | |
|
83 | class="btn" | |
|
84 | href="#" | |
|
85 | onclick="$('input[class=diff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a> | |
|
86 | </div> | |
|
87 | </div> | |
|
88 | %endif | |
|
89 | <h2 style="padding: 5px; text-align: center;"> | |
|
90 | %if diffset.limited_diff: | |
|
91 | ${ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files}} | |
|
92 | %else: | |
|
93 | ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', | |
|
94 | '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}} | |
|
95 | %endif | |
|
96 | </h2> | |
|
97 | </div> | |
|
98 | ||
|
99 | %if not diffset.files: | |
|
100 | <p class="empty_data">${_('No files')}</p> | |
|
101 | %endif | |
|
102 | ||
|
103 | <div class="filediffs"> | |
|
104 | %for i, filediff in enumerate(diffset.files): | |
|
105 | <% | |
|
106 | lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted'] | |
|
107 | over_lines_changed_limit = lines_changed > lines_changed_limit | |
|
108 | %> | |
|
109 | <input ${collapse_all and 'checked' or ''} class="diff-collapse-state" id="diff-collapse-${i}" type="checkbox"> | |
|
110 | <div | |
|
111 | class="diff" | |
|
112 | data-f-path="${filediff['patch']['filename']}" | |
|
113 | id="a_${h.FID('', filediff['patch']['filename'])}"> | |
|
114 | <label for="diff-collapse-${i}" class="diff-heading"> | |
|
115 | <div class="diff-collapse-indicator"></div> | |
|
116 | ${diff_ops(filediff)} | |
|
117 | </label> | |
|
118 | ${diff_menu(filediff)} | |
|
119 | <table class="cb cb-diff-${diff_mode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}"> | |
|
120 | %if not filediff.hunks: | |
|
121 | %for op_id, op_text in filediff['patch']['stats']['ops'].items(): | |
|
122 | <tr> | |
|
123 | <td class="cb-text cb-${op_class(op_id)}" ${diff_mode == 'unified' and 'colspan=3' or 'colspan=4'}> | |
|
124 | %if op_id == DEL_FILENODE: | |
|
125 | ${_('File was deleted')} | |
|
126 | %elif op_id == BIN_FILENODE: | |
|
127 | ${_('Binary file hidden')} | |
|
128 | %else: | |
|
129 | ${op_text} | |
|
130 | %endif | |
|
131 | </td> | |
|
132 | </tr> | |
|
133 | %endfor | |
|
134 | %endif | |
|
135 | %if over_lines_changed_limit: | |
|
136 | <tr class="cb-warning cb-collapser"> | |
|
137 | <td class="cb-text" ${diff_mode == 'unified' and 'colspan=3' or 'colspan=4'}> | |
|
138 | ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)} | |
|
139 | <a href="#" class="cb-expand" | |
|
140 | onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')} | |
|
141 | </a> | |
|
142 | <a href="#" class="cb-collapse" | |
|
143 | onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')} | |
|
144 | </a> | |
|
145 | </td> | |
|
146 | </tr> | |
|
147 | %endif | |
|
148 | %if filediff.patch['is_limited_diff']: | |
|
149 | <tr class="cb-warning cb-collapser"> | |
|
150 | <td class="cb-text" ${diff_mode == 'unified' and 'colspan=3' or 'colspan=4'}> | |
|
151 | ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a> | |
|
152 | </td> | |
|
153 | </tr> | |
|
154 | %endif | |
|
155 | %for hunk in filediff.hunks: | |
|
156 | <tr class="cb-hunk"> | |
|
157 | <td ${diff_mode == 'unified' and 'colspan=2' or ''}> | |
|
158 | ## TODO: dan: add ajax loading of more context here | |
|
159 | ## <a href="#"> | |
|
160 | <i class="icon-more"></i> | |
|
161 | ## </a> | |
|
162 | </td> | |
|
163 | <td ${diff_mode == 'sideside' and 'colspan=3' or ''}> | |
|
164 | @@ | |
|
165 | -${hunk.source_start},${hunk.source_length} | |
|
166 | +${hunk.target_start},${hunk.target_length} | |
|
167 | ${hunk.section_header} | |
|
168 | </td> | |
|
169 | </tr> | |
|
170 | %if diff_mode == 'unified': | |
|
171 | ${render_hunk_lines_unified(hunk)} | |
|
172 | %elif diff_mode == 'sideside': | |
|
173 | ${render_hunk_lines_sideside(hunk)} | |
|
174 | %else: | |
|
175 | <tr class="cb-line"> | |
|
176 | <td>unknown diff mode</td> | |
|
177 | </tr> | |
|
178 | %endif | |
|
179 | %endfor | |
|
180 | </table> | |
|
181 | </div> | |
|
182 | %endfor | |
|
183 | </div> | |
|
184 | </div> | |
|
185 | </%def> | |
|
186 | ||
|
187 | <%def name="diff_ops(filediff)"> | |
|
188 | <% | |
|
189 | stats = filediff['patch']['stats'] | |
|
190 | from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \ | |
|
191 | MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE | |
|
192 | %> | |
|
193 | <span class="diff-pill"> | |
|
194 | %if filediff.source_file_path and filediff.target_file_path: | |
|
195 | %if filediff.source_file_path != filediff.target_file_path: # file was renamed | |
|
196 | <strong>${filediff.target_file_path}</strong> ⬅ <del>${filediff.source_file_path}</del> | |
|
197 | %else: | |
|
198 | ## file was modified | |
|
199 | <strong>${filediff.source_file_path}</strong> | |
|
200 | %endif | |
|
201 | %else: | |
|
202 | %if filediff.source_file_path: | |
|
203 | ## file was deleted | |
|
204 | <strong>${filediff.source_file_path}</strong> | |
|
205 | %else: | |
|
206 | ## file was added | |
|
207 | <strong>${filediff.target_file_path}</strong> | |
|
208 | %endif | |
|
209 | %endif | |
|
210 | </span> | |
|
211 | <span class="diff-pill-group" style="float: left"> | |
|
212 | %if filediff.patch['is_limited_diff']: | |
|
213 | <span class="diff-pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span> | |
|
214 | %endif | |
|
215 | %if RENAMED_FILENODE in stats['ops']: | |
|
216 | <span class="diff-pill" op="renamed">renamed</span> | |
|
217 | %endif | |
|
218 | ||
|
219 | %if NEW_FILENODE in stats['ops']: | |
|
220 | <span class="diff-pill" op="created">created</span> | |
|
221 | %if filediff['target_mode'].startswith('120'): | |
|
222 | <span class="diff-pill" op="symlink">symlink</span> | |
|
223 | %else: | |
|
224 | <span class="diff-pill" op="mode">${nice_mode(filediff['target_mode'])}</span> | |
|
225 | %endif | |
|
226 | %endif | |
|
227 | ||
|
228 | %if DEL_FILENODE in stats['ops']: | |
|
229 | <span class="diff-pill" op="removed">removed</span> | |
|
230 | %endif | |
|
231 | ||
|
232 | %if CHMOD_FILENODE in stats['ops']: | |
|
233 | <span class="diff-pill" op="mode"> | |
|
234 | ${nice_mode(filediff['source_mode'])} ➡ ${nice_mode(filediff['target_mode'])} | |
|
235 | </span> | |
|
236 | %endif | |
|
237 | </span> | |
|
238 | ||
|
239 | <a class="diff-pill diff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">¶</a> | |
|
240 | ||
|
241 | <span class="diff-pill-group" style="float: right"> | |
|
242 | %if BIN_FILENODE in stats['ops']: | |
|
243 | <span class="diff-pill" op="binary">binary</span> | |
|
244 | %if MOD_FILENODE in stats['ops']: | |
|
245 | <span class="diff-pill" op="modified">modified</span> | |
|
246 | %endif | |
|
247 | %endif | |
|
248 | %if stats['deleted']: | |
|
249 | <span class="diff-pill" op="deleted">-${stats['deleted']}</span> | |
|
250 | %endif | |
|
251 | %if stats['added']: | |
|
252 | <span class="diff-pill" op="added">+${stats['added']}</span> | |
|
253 | %endif | |
|
254 | </span> | |
|
255 | ||
|
256 | </%def> | |
|
257 | ||
|
258 | <%def name="nice_mode(filemode)"> | |
|
259 | ${filemode.startswith('100') and filemode[3:] or filemode} | |
|
260 | </%def> | |
|
261 | ||
|
262 | <%def name="diff_menu(filediff)"> | |
|
263 | <div class="diff-menu"> | |
|
264 | %if filediff.diffset.source_ref: | |
|
265 | %if filediff.patch['operation'] in ['D', 'M']: | |
|
266 | <a | |
|
267 | class="tooltip" | |
|
268 | href="${h.url('files_home',repo_name=c.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}" | |
|
269 | title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}" | |
|
270 | > | |
|
271 | ${_('Show file before')} | |
|
272 | </a> | |
|
273 | %else: | |
|
274 | <a | |
|
275 | disabled | |
|
276 | class="tooltip" | |
|
277 | title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}" | |
|
278 | > | |
|
279 | ${_('Show file before')} | |
|
280 | </a> | |
|
281 | %endif | |
|
282 | %if filediff.patch['operation'] in ['A', 'M']: | |
|
283 | <a | |
|
284 | class="tooltip" | |
|
285 | href="${h.url('files_home',repo_name=c.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}" | |
|
286 | title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}" | |
|
287 | > | |
|
288 | ${_('Show file after')} | |
|
289 | </a> | |
|
290 | %else: | |
|
291 | <a | |
|
292 | disabled | |
|
293 | class="tooltip" | |
|
294 | title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}" | |
|
295 | > | |
|
296 | ${_('Show file after')} | |
|
297 | </a> | |
|
298 | %endif | |
|
299 | <a | |
|
300 | class="tooltip" | |
|
301 | title="${h.tooltip(_('Raw diff'))}" | |
|
302 | href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}" | |
|
303 | > | |
|
304 | ${_('Raw diff')} | |
|
305 | </a> | |
|
306 | <a | |
|
307 | class="tooltip" | |
|
308 | title="${h.tooltip(_('Download diff'))}" | |
|
309 | href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}" | |
|
310 | > | |
|
311 | ${_('Download diff')} | |
|
312 | </a> | |
|
313 | %endif | |
|
314 | </div> | |
|
315 | </%def> | |
|
316 | ||
|
317 | ||
|
318 | <%def name="render_hunk_lines_sideside(hunk)"> | |
|
319 | %for i, line in enumerate(hunk.sideside): | |
|
320 | <% | |
|
321 | old_line_anchor, new_line_anchor = None, None | |
|
322 | if line.original.lineno: | |
|
323 | old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o') | |
|
324 | if line.modified.lineno: | |
|
325 | new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n') | |
|
326 | %> | |
|
327 | <tr class="cb-line"> | |
|
328 | <td class="cb-lineno ${action_class(line.original.action)}" | |
|
329 | data-line-number="${line.original.lineno}" | |
|
330 | %if old_line_anchor: | |
|
331 | id="${old_line_anchor}" | |
|
332 | %endif | |
|
333 | > | |
|
334 | %if line.original.lineno: | |
|
335 | <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a> | |
|
336 | %endif | |
|
337 | </td> | |
|
338 | <td class="cb-content ${action_class(line.original.action)}" | |
|
339 | data-line-number="o${line.original.lineno}" | |
|
340 | ><span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span> | |
|
341 | </td> | |
|
342 | <td class="cb-lineno ${action_class(line.modified.action)}" | |
|
343 | data-line-number="${line.modified.lineno}" | |
|
344 | %if new_line_anchor: | |
|
345 | id="${new_line_anchor}" | |
|
346 | %endif | |
|
347 | > | |
|
348 | %if line.modified.lineno: | |
|
349 | <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a> | |
|
350 | %endif | |
|
351 | </td> | |
|
352 | <td class="cb-content ${action_class(line.modified.action)}" | |
|
353 | data-line-number="n${line.modified.lineno}" | |
|
354 | > | |
|
355 | <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span> | |
|
356 | </td> | |
|
357 | </tr> | |
|
358 | %endfor | |
|
359 | </%def> | |
|
360 | ||
|
361 | ||
|
362 | <%def name="render_hunk_lines_unified(hunk)"> | |
|
363 | %for old_line_no, new_line_no, action, content in hunk.unified: | |
|
364 | <% | |
|
365 | old_line_anchor, new_line_anchor = None, None | |
|
366 | if old_line_no: | |
|
367 | old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o') | |
|
368 | if new_line_no: | |
|
369 | new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n') | |
|
370 | %> | |
|
371 | <tr class="cb-line"> | |
|
372 | <td class="cb-lineno ${action_class(action)}" | |
|
373 | data-line-number="${old_line_no}" | |
|
374 | %if old_line_anchor: | |
|
375 | id="${old_line_anchor}" | |
|
376 | %endif | |
|
377 | > | |
|
378 | %if old_line_anchor: | |
|
379 | <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a> | |
|
380 | %endif | |
|
381 | </td> | |
|
382 | <td class="cb-lineno ${action_class(action)}" | |
|
383 | data-line-number="${new_line_no}" | |
|
384 | %if new_line_anchor: | |
|
385 | id="${new_line_anchor}" | |
|
386 | %endif | |
|
387 | > | |
|
388 | %if new_line_anchor: | |
|
389 | <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a> | |
|
390 | %endif | |
|
391 | </td> | |
|
392 | <td class="cb-content ${action_class(action)}" | |
|
393 | data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}" | |
|
394 | ><span class="cb-code">${action} ${content or '' | n}</span> | |
|
395 | </td> | |
|
396 | </tr> | |
|
397 | %endfor | |
|
398 | </%def> |
@@ -14,6 +14,8 b' permission notice:' | |||
|
14 | 14 | file:licenses/tornado_license.txt |
|
15 | 15 | Copyright (c) 2015 - pygments-markdown-lexer |
|
16 | 16 | file:licenses/pygments_markdown_lexer_license.txt |
|
17 | Copyright 2006 - diff_match_patch | |
|
18 | file:licenses/diff_match_patch_license.txt | |
|
17 | 19 | |
|
18 | 20 | All licensed under the Apache License, Version 2.0 (the "License"); |
|
19 | 21 | you may not use this file except in compliance with the License. |
@@ -31,13 +31,14 b' from pylons.i18n.translation import _' | |||
|
31 | 31 | |
|
32 | 32 | from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name |
|
33 | 33 | from rhodecode.lib import helpers as h |
|
34 | from rhodecode.lib import diffs | |
|
34 | from rhodecode.lib import diffs, codeblocks | |
|
35 | 35 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
36 | 36 | from rhodecode.lib.base import BaseRepoController, render |
|
37 | 37 | from rhodecode.lib.utils import safe_str |
|
38 | 38 | from rhodecode.lib.utils2 import safe_unicode, str2bool |
|
39 | 39 | from rhodecode.lib.vcs.exceptions import ( |
|
40 |
EmptyRepositoryError, RepositoryError, RepositoryRequirementError |
|
|
40 | EmptyRepositoryError, RepositoryError, RepositoryRequirementError, | |
|
41 | NodeDoesNotExistError) | |
|
41 | 42 | from rhodecode.model.db import Repository, ChangesetStatus |
|
42 | 43 | |
|
43 | 44 | log = logging.getLogger(__name__) |
@@ -78,7 +79,7 b' class CompareController(BaseRepoControll' | |||
|
78 | 79 | def index(self, repo_name): |
|
79 | 80 | c.compare_home = True |
|
80 | 81 | c.commit_ranges = [] |
|
81 |
c. |
|
|
82 | c.diffset = None | |
|
82 | 83 | c.limited_diff = False |
|
83 | 84 | source_repo = c.rhodecode_db_repo.repo_name |
|
84 | 85 | target_repo = request.GET.get('target_repo', source_repo) |
@@ -239,28 +240,23 b' class CompareController(BaseRepoControll' | |||
|
239 | 240 | commit1=source_commit, commit2=target_commit, |
|
240 | 241 | path1=source_path, path=target_path) |
|
241 | 242 | diff_processor = diffs.DiffProcessor( |
|
242 |
txtdiff, format=' |
|
|
243 | txtdiff, format='newdiff', diff_limit=diff_limit, | |
|
243 | 244 | file_limit=file_limit, show_full_diff=c.fulldiff) |
|
244 | 245 | _parsed = diff_processor.prepare() |
|
245 | 246 | |
|
246 | c.limited_diff = False | |
|
247 | if isinstance(_parsed, diffs.LimitedDiffContainer): | |
|
248 | c.limited_diff = True | |
|
247 | def _node_getter(commit): | |
|
248 | """ Returns a function that returns a node for a commit or None """ | |
|
249 | def get_node(fname): | |
|
250 | try: | |
|
251 | return commit.get_node(fname) | |
|
252 | except NodeDoesNotExistError: | |
|
253 | return None | |
|
254 | return get_node | |
|
249 | 255 | |
|
250 | c.files = [] | |
|
251 | c.changes = {} | |
|
252 | c.lines_added = 0 | |
|
253 | c.lines_deleted = 0 | |
|
254 | for f in _parsed: | |
|
255 | st = f['stats'] | |
|
256 | if not st['binary']: | |
|
257 | c.lines_added += st['added'] | |
|
258 | c.lines_deleted += st['deleted'] | |
|
259 | fid = h.FID('', f['filename']) | |
|
260 | c.files.append([fid, f['operation'], f['filename'], f['stats'], f]) | |
|
261 | htmldiff = diff_processor.as_html( | |
|
262 | enable_comments=False, parsed_lines=[f]) | |
|
263 | c.changes[fid] = [f['operation'], f['filename'], htmldiff, f] | |
|
256 | c.diffset = codeblocks.DiffSet( | |
|
257 | source_node_getter=_node_getter(source_commit), | |
|
258 | target_node_getter=_node_getter(target_commit), | |
|
259 | ).render_patchset(_parsed, source_ref, target_ref) | |
|
264 | 260 | |
|
265 | 261 | c.preview_mode = merge |
|
266 | 262 |
@@ -19,13 +19,17 b'' | |||
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | import difflib | |
|
22 | 23 | from itertools import groupby |
|
23 | 24 | |
|
24 | 25 | from pygments import lex |
|
25 | 26 | from pygments.formatters.html import _get_ttype_class as pygment_token_class |
|
26 |
from rhodecode.lib.helpers import |
|
|
27 | from rhodecode.lib.helpers import ( | |
|
28 | get_lexer_for_filenode, get_lexer_safe, html_escape) | |
|
27 | 29 | from rhodecode.lib.utils2 import AttributeDict |
|
28 | 30 | from rhodecode.lib.vcs.nodes import FileNode |
|
31 | from rhodecode.lib.diff_match_patch import diff_match_patch | |
|
32 | from rhodecode.lib.diffs import LimitedDiffContainer | |
|
29 | 33 | from pygments.lexers import get_lexer_by_name |
|
30 | 34 | |
|
31 | 35 | plain_text_lexer = get_lexer_by_name( |
@@ -38,7 +42,7 b' log = logging.getLogger()' | |||
|
38 | 42 | def filenode_as_lines_tokens(filenode, lexer=None): |
|
39 | 43 | lexer = lexer or get_lexer_for_filenode(filenode) |
|
40 | 44 | log.debug('Generating file node pygment tokens for %s, %s', lexer, filenode) |
|
41 |
tokens = tokenize_string(filenode.content, |
|
|
45 | tokens = tokenize_string(filenode.content, lexer) | |
|
42 | 46 | lines = split_token_stream(tokens, split_string='\n') |
|
43 | 47 | rv = list(lines) |
|
44 | 48 | return rv |
@@ -146,7 +150,11 b' def render_tokenstream(tokenstream):' | |||
|
146 | 150 | result.append(u'<%s>' % op_tag) |
|
147 | 151 | |
|
148 | 152 | escaped_text = html_escape(token_text) |
|
149 | escaped_text = escaped_text.replace('\n', '<nl>\n</nl>') | |
|
153 | ||
|
154 | # TODO: dan: investigate showing hidden characters like space/nl/tab | |
|
155 | # escaped_text = escaped_text.replace(' ', '<sp> </sp>') | |
|
156 | # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>') | |
|
157 | # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>') | |
|
150 | 158 | |
|
151 | 159 | result.append(escaped_text) |
|
152 | 160 | |
@@ -212,3 +220,416 b' def rollup_tokenstream(tokenstream):' | |||
|
212 | 220 | ops.append((token_op, ''.join(text_buffer))) |
|
213 | 221 | result.append((token_class, ops)) |
|
214 | 222 | return result |
|
223 | ||
|
224 | ||
|
225 | def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True): | |
|
226 | """ | |
|
227 | Converts a list of (token_class, token_text) tuples to a list of | |
|
228 | (token_class, token_op, token_text) tuples where token_op is one of | |
|
229 | ('ins', 'del', '') | |
|
230 | ||
|
231 | :param old_tokens: list of (token_class, token_text) tuples of old line | |
|
232 | :param new_tokens: list of (token_class, token_text) tuples of new line | |
|
233 | :param use_diff_match_patch: boolean, will use google's diff match patch | |
|
234 | library which has options to 'smooth' out the character by character | |
|
235 | differences making nicer ins/del blocks | |
|
236 | """ | |
|
237 | ||
|
238 | old_tokens_result = [] | |
|
239 | new_tokens_result = [] | |
|
240 | ||
|
241 | similarity = difflib.SequenceMatcher(None, | |
|
242 | ''.join(token_text for token_class, token_text in old_tokens), | |
|
243 | ''.join(token_text for token_class, token_text in new_tokens) | |
|
244 | ).ratio() | |
|
245 | ||
|
246 | if similarity < 0.6: # return, the blocks are too different | |
|
247 | for token_class, token_text in old_tokens: | |
|
248 | old_tokens_result.append((token_class, '', token_text)) | |
|
249 | for token_class, token_text in new_tokens: | |
|
250 | new_tokens_result.append((token_class, '', token_text)) | |
|
251 | return old_tokens_result, new_tokens_result, similarity | |
|
252 | ||
|
253 | token_sequence_matcher = difflib.SequenceMatcher(None, | |
|
254 | [x[1] for x in old_tokens], | |
|
255 | [x[1] for x in new_tokens]) | |
|
256 | ||
|
257 | for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes(): | |
|
258 | # check the differences by token block types first to give a more | |
|
259 | # nicer "block" level replacement vs character diffs | |
|
260 | ||
|
261 | if tag == 'equal': | |
|
262 | for token_class, token_text in old_tokens[o1:o2]: | |
|
263 | old_tokens_result.append((token_class, '', token_text)) | |
|
264 | for token_class, token_text in new_tokens[n1:n2]: | |
|
265 | new_tokens_result.append((token_class, '', token_text)) | |
|
266 | elif tag == 'delete': | |
|
267 | for token_class, token_text in old_tokens[o1:o2]: | |
|
268 | old_tokens_result.append((token_class, 'del', token_text)) | |
|
269 | elif tag == 'insert': | |
|
270 | for token_class, token_text in new_tokens[n1:n2]: | |
|
271 | new_tokens_result.append((token_class, 'ins', token_text)) | |
|
272 | elif tag == 'replace': | |
|
273 | # if same type token blocks must be replaced, do a diff on the | |
|
274 | # characters in the token blocks to show individual changes | |
|
275 | ||
|
276 | old_char_tokens = [] | |
|
277 | new_char_tokens = [] | |
|
278 | for token_class, token_text in old_tokens[o1:o2]: | |
|
279 | for char in token_text: | |
|
280 | old_char_tokens.append((token_class, char)) | |
|
281 | ||
|
282 | for token_class, token_text in new_tokens[n1:n2]: | |
|
283 | for char in token_text: | |
|
284 | new_char_tokens.append((token_class, char)) | |
|
285 | ||
|
286 | old_string = ''.join([token_text for | |
|
287 | token_class, token_text in old_char_tokens]) | |
|
288 | new_string = ''.join([token_text for | |
|
289 | token_class, token_text in new_char_tokens]) | |
|
290 | ||
|
291 | char_sequence = difflib.SequenceMatcher( | |
|
292 | None, old_string, new_string) | |
|
293 | copcodes = char_sequence.get_opcodes() | |
|
294 | obuffer, nbuffer = [], [] | |
|
295 | ||
|
296 | if use_diff_match_patch: | |
|
297 | dmp = diff_match_patch() | |
|
298 | dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting | |
|
299 | reps = dmp.diff_main(old_string, new_string) | |
|
300 | dmp.diff_cleanupEfficiency(reps) | |
|
301 | ||
|
302 | a, b = 0, 0 | |
|
303 | for op, rep in reps: | |
|
304 | l = len(rep) | |
|
305 | if op == 0: | |
|
306 | for i, c in enumerate(rep): | |
|
307 | obuffer.append((old_char_tokens[a+i][0], '', c)) | |
|
308 | nbuffer.append((new_char_tokens[b+i][0], '', c)) | |
|
309 | a += l | |
|
310 | b += l | |
|
311 | elif op == -1: | |
|
312 | for i, c in enumerate(rep): | |
|
313 | obuffer.append((old_char_tokens[a+i][0], 'del', c)) | |
|
314 | a += l | |
|
315 | elif op == 1: | |
|
316 | for i, c in enumerate(rep): | |
|
317 | nbuffer.append((new_char_tokens[b+i][0], 'ins', c)) | |
|
318 | b += l | |
|
319 | else: | |
|
320 | for ctag, co1, co2, cn1, cn2 in copcodes: | |
|
321 | if ctag == 'equal': | |
|
322 | for token_class, token_text in old_char_tokens[co1:co2]: | |
|
323 | obuffer.append((token_class, '', token_text)) | |
|
324 | for token_class, token_text in new_char_tokens[cn1:cn2]: | |
|
325 | nbuffer.append((token_class, '', token_text)) | |
|
326 | elif ctag == 'delete': | |
|
327 | for token_class, token_text in old_char_tokens[co1:co2]: | |
|
328 | obuffer.append((token_class, 'del', token_text)) | |
|
329 | elif ctag == 'insert': | |
|
330 | for token_class, token_text in new_char_tokens[cn1:cn2]: | |
|
331 | nbuffer.append((token_class, 'ins', token_text)) | |
|
332 | elif ctag == 'replace': | |
|
333 | for token_class, token_text in old_char_tokens[co1:co2]: | |
|
334 | obuffer.append((token_class, 'del', token_text)) | |
|
335 | for token_class, token_text in new_char_tokens[cn1:cn2]: | |
|
336 | nbuffer.append((token_class, 'ins', token_text)) | |
|
337 | ||
|
338 | old_tokens_result.extend(obuffer) | |
|
339 | new_tokens_result.extend(nbuffer) | |
|
340 | ||
|
341 | return old_tokens_result, new_tokens_result, similarity | |
|
342 | ||
|
343 | ||
|
344 | class DiffSet(object): | |
|
345 | """ | |
|
346 | An object for parsing the diff result from diffs.DiffProcessor and | |
|
347 | adding highlighting, side by side/unified renderings and line diffs | |
|
348 | """ | |
|
349 | ||
|
350 | HL_REAL = 'REAL' # highlights using original file, slow | |
|
351 | HL_FAST = 'FAST' # highlights using just the line, fast but not correct | |
|
352 | # in the case of multiline code | |
|
353 | HL_NONE = 'NONE' # no highlighting, fastest | |
|
354 | ||
|
355 | def __init__(self, highlight_mode=HL_REAL, | |
|
356 | source_node_getter=lambda filename: None, | |
|
357 | target_node_getter=lambda filename: None, | |
|
358 | source_nodes=None, target_nodes=None, | |
|
359 | max_file_size_limit=150 * 1024, # files over this size will | |
|
360 | # use fast highlighting | |
|
361 | ): | |
|
362 | ||
|
363 | self.highlight_mode = highlight_mode | |
|
364 | self.highlighted_filenodes = {} | |
|
365 | self.source_node_getter = source_node_getter | |
|
366 | self.target_node_getter = target_node_getter | |
|
367 | self.source_nodes = source_nodes or {} | |
|
368 | self.target_nodes = target_nodes or {} | |
|
369 | ||
|
370 | ||
|
371 | self.max_file_size_limit = max_file_size_limit | |
|
372 | ||
|
373 | def render_patchset(self, patchset, source_ref=None, target_ref=None): | |
|
374 | diffset = AttributeDict(dict( | |
|
375 | lines_added=0, | |
|
376 | lines_deleted=0, | |
|
377 | changed_files=0, | |
|
378 | files=[], | |
|
379 | limited_diff=isinstance(patchset, LimitedDiffContainer), | |
|
380 | source_ref=source_ref, | |
|
381 | target_ref=target_ref, | |
|
382 | )) | |
|
383 | for patch in patchset: | |
|
384 | filediff = self.render_patch(patch) | |
|
385 | filediff.diffset = diffset | |
|
386 | diffset.files.append(filediff) | |
|
387 | diffset.changed_files += 1 | |
|
388 | if not patch['stats']['binary']: | |
|
389 | diffset.lines_added += patch['stats']['added'] | |
|
390 | diffset.lines_deleted += patch['stats']['deleted'] | |
|
391 | ||
|
392 | return diffset | |
|
393 | ||
|
394 | _lexer_cache = {} | |
|
395 | def _get_lexer_for_filename(self, filename): | |
|
396 | # cached because we might need to call it twice for source/target | |
|
397 | if filename not in self._lexer_cache: | |
|
398 | self._lexer_cache[filename] = get_lexer_safe(filepath=filename) | |
|
399 | return self._lexer_cache[filename] | |
|
400 | ||
|
401 | def render_patch(self, patch): | |
|
402 | log.debug('rendering diff for %r' % patch['filename']) | |
|
403 | ||
|
404 | source_filename = patch['original_filename'] | |
|
405 | target_filename = patch['filename'] | |
|
406 | ||
|
407 | source_lexer = plain_text_lexer | |
|
408 | target_lexer = plain_text_lexer | |
|
409 | ||
|
410 | if not patch['stats']['binary']: | |
|
411 | if self.highlight_mode == self.HL_REAL: | |
|
412 | if (source_filename and patch['operation'] in ('D', 'M') | |
|
413 | and source_filename not in self.source_nodes): | |
|
414 | self.source_nodes[source_filename] = ( | |
|
415 | self.source_node_getter(source_filename)) | |
|
416 | ||
|
417 | if (target_filename and patch['operation'] in ('A', 'M') | |
|
418 | and target_filename not in self.target_nodes): | |
|
419 | self.target_nodes[target_filename] = ( | |
|
420 | self.target_node_getter(target_filename)) | |
|
421 | ||
|
422 | elif self.highlight_mode == self.HL_FAST: | |
|
423 | source_lexer = self._get_lexer_for_filename(source_filename) | |
|
424 | target_lexer = self._get_lexer_for_filename(target_filename) | |
|
425 | ||
|
426 | source_file = self.source_nodes.get(source_filename, source_filename) | |
|
427 | target_file = self.target_nodes.get(target_filename, target_filename) | |
|
428 | ||
|
429 | source_filenode, target_filenode = None, None | |
|
430 | ||
|
431 | # TODO: dan: FileNode.lexer works on the content of the file - which | |
|
432 | # can be slow - issue #4289 explains a lexer clean up - which once | |
|
433 | # done can allow caching a lexer for a filenode to avoid the file lookup | |
|
434 | if isinstance(source_file, FileNode): | |
|
435 | source_filenode = source_file | |
|
436 | source_lexer = source_file.lexer | |
|
437 | if isinstance(target_file, FileNode): | |
|
438 | target_filenode = target_file | |
|
439 | target_lexer = target_file.lexer | |
|
440 | ||
|
441 | source_file_path, target_file_path = None, None | |
|
442 | ||
|
443 | if source_filename != '/dev/null': | |
|
444 | source_file_path = source_filename | |
|
445 | if target_filename != '/dev/null': | |
|
446 | target_file_path = target_filename | |
|
447 | ||
|
448 | source_file_type = source_lexer.name | |
|
449 | target_file_type = target_lexer.name | |
|
450 | ||
|
451 | op_hunks = patch['chunks'][0] | |
|
452 | hunks = patch['chunks'][1:] | |
|
453 | ||
|
454 | filediff = AttributeDict({ | |
|
455 | 'source_file_path': source_file_path, | |
|
456 | 'target_file_path': target_file_path, | |
|
457 | 'source_filenode': source_filenode, | |
|
458 | 'target_filenode': target_filenode, | |
|
459 | 'hunks': [], | |
|
460 | 'source_file_type': target_file_type, | |
|
461 | 'target_file_type': source_file_type, | |
|
462 | 'patch': patch, | |
|
463 | 'source_mode': patch['stats']['old_mode'], | |
|
464 | 'target_mode': patch['stats']['new_mode'], | |
|
465 | 'limited_diff': isinstance(patch, LimitedDiffContainer), | |
|
466 | 'diffset': self, | |
|
467 | }) | |
|
468 | ||
|
469 | for hunk in hunks: | |
|
470 | hunkbit = self.parse_hunk(hunk, source_file, target_file) | |
|
471 | hunkbit.filediff = filediff | |
|
472 | filediff.hunks.append(hunkbit) | |
|
473 | return filediff | |
|
474 | ||
|
475 | def parse_hunk(self, hunk, source_file, target_file): | |
|
476 | result = AttributeDict(dict( | |
|
477 | source_start=hunk['source_start'], | |
|
478 | source_length=hunk['source_length'], | |
|
479 | target_start=hunk['target_start'], | |
|
480 | target_length=hunk['target_length'], | |
|
481 | section_header=hunk['section_header'], | |
|
482 | lines=[], | |
|
483 | )) | |
|
484 | before, after = [], [] | |
|
485 | ||
|
486 | for line in hunk['lines']: | |
|
487 | if line['action'] == 'unmod': | |
|
488 | result.lines.extend( | |
|
489 | self.parse_lines(before, after, source_file, target_file)) | |
|
490 | after.append(line) | |
|
491 | before.append(line) | |
|
492 | elif line['action'] == 'add': | |
|
493 | after.append(line) | |
|
494 | elif line['action'] == 'del': | |
|
495 | before.append(line) | |
|
496 | elif line['action'] == 'context-old': | |
|
497 | before.append(line) | |
|
498 | elif line['action'] == 'context-new': | |
|
499 | after.append(line) | |
|
500 | ||
|
501 | result.lines.extend( | |
|
502 | self.parse_lines(before, after, source_file, target_file)) | |
|
503 | result.unified = self.as_unified(result.lines) | |
|
504 | result.sideside = result.lines | |
|
505 | return result | |
|
506 | ||
|
507 | def parse_lines(self, before_lines, after_lines, source_file, target_file): | |
|
508 | # TODO: dan: investigate doing the diff comparison and fast highlighting | |
|
509 | # on the entire before and after buffered block lines rather than by | |
|
510 | # line, this means we can get better 'fast' highlighting if the context | |
|
511 | # allows it - eg. | |
|
512 | # line 4: """ | |
|
513 | # line 5: this gets highlighted as a string | |
|
514 | # line 6: """ | |
|
515 | ||
|
516 | lines = [] | |
|
517 | while before_lines or after_lines: | |
|
518 | before, after = None, None | |
|
519 | before_tokens, after_tokens = None, None | |
|
520 | ||
|
521 | if before_lines: | |
|
522 | before = before_lines.pop(0) | |
|
523 | if after_lines: | |
|
524 | after = after_lines.pop(0) | |
|
525 | ||
|
526 | original = AttributeDict() | |
|
527 | modified = AttributeDict() | |
|
528 | ||
|
529 | if before: | |
|
530 | before_tokens = self.get_line_tokens( | |
|
531 | line_text=before['line'], line_number=before['old_lineno'], | |
|
532 | file=source_file) | |
|
533 | original.lineno = before['old_lineno'] | |
|
534 | original.content = before['line'] | |
|
535 | original.action = self.action_to_op(before['action']) | |
|
536 | ||
|
537 | if after: | |
|
538 | after_tokens = self.get_line_tokens( | |
|
539 | line_text=after['line'], line_number=after['new_lineno'], | |
|
540 | file=target_file) | |
|
541 | modified.lineno = after['new_lineno'] | |
|
542 | modified.content = after['line'] | |
|
543 | modified.action = self.action_to_op(after['action']) | |
|
544 | ||
|
545 | ||
|
546 | # diff the lines | |
|
547 | if before_tokens and after_tokens: | |
|
548 | o_tokens, m_tokens, similarity = tokens_diff(before_tokens, after_tokens) | |
|
549 | original.content = render_tokenstream(o_tokens) | |
|
550 | modified.content = render_tokenstream(m_tokens) | |
|
551 | elif before_tokens: | |
|
552 | original.content = render_tokenstream( | |
|
553 | [(x[0], '', x[1]) for x in before_tokens]) | |
|
554 | elif after_tokens: | |
|
555 | modified.content = render_tokenstream( | |
|
556 | [(x[0], '', x[1]) for x in after_tokens]) | |
|
557 | ||
|
558 | lines.append(AttributeDict({ | |
|
559 | 'original': original, | |
|
560 | 'modified': modified, | |
|
561 | })) | |
|
562 | ||
|
563 | return lines | |
|
564 | ||
|
565 | def get_line_tokens(self, line_text, line_number, file=None): | |
|
566 | filenode = None | |
|
567 | filename = None | |
|
568 | ||
|
569 | if isinstance(file, basestring): | |
|
570 | filename = file | |
|
571 | elif isinstance(file, FileNode): | |
|
572 | filenode = file | |
|
573 | filename = file.unicode_path | |
|
574 | ||
|
575 | if self.highlight_mode == self.HL_REAL and filenode: | |
|
576 | if line_number and file.size < self.max_file_size_limit: | |
|
577 | return self.get_tokenized_filenode_line(file, line_number) | |
|
578 | ||
|
579 | if self.highlight_mode in (self.HL_REAL, self.HL_FAST) and filename: | |
|
580 | lexer = self._get_lexer_for_filename(filename) | |
|
581 | return list(tokenize_string(line_text, lexer)) | |
|
582 | ||
|
583 | return list(tokenize_string(line_text, plain_text_lexer)) | |
|
584 | ||
|
585 | def get_tokenized_filenode_line(self, filenode, line_number): | |
|
586 | ||
|
587 | if filenode not in self.highlighted_filenodes: | |
|
588 | tokenized_lines = filenode_as_lines_tokens(filenode, filenode.lexer) | |
|
589 | self.highlighted_filenodes[filenode] = tokenized_lines | |
|
590 | return self.highlighted_filenodes[filenode][line_number - 1] | |
|
591 | ||
|
592 | def action_to_op(self, action): | |
|
593 | return { | |
|
594 | 'add': '+', | |
|
595 | 'del': '-', | |
|
596 | 'unmod': ' ', | |
|
597 | 'context-old': ' ', | |
|
598 | 'context-new': ' ', | |
|
599 | }.get(action, action) | |
|
600 | ||
|
601 | def as_unified(self, lines): | |
|
602 | """ Return a generator that yields the lines of a diff in unified order """ | |
|
603 | def generator(): | |
|
604 | buf = [] | |
|
605 | for line in lines: | |
|
606 | ||
|
607 | if buf and not line.original or line.original.action == ' ': | |
|
608 | for b in buf: | |
|
609 | yield b | |
|
610 | buf = [] | |
|
611 | ||
|
612 | if line.original: | |
|
613 | if line.original.action == ' ': | |
|
614 | yield (line.original.lineno, line.modified.lineno, | |
|
615 | line.original.action, line.original.content) | |
|
616 | continue | |
|
617 | ||
|
618 | if line.original.action == '-': | |
|
619 | yield (line.original.lineno, None, | |
|
620 | line.original.action, line.original.content) | |
|
621 | ||
|
622 | if line.modified.action == '+': | |
|
623 | buf.append(( | |
|
624 | None, line.modified.lineno, | |
|
625 | line.modified.action, line.modified.content)) | |
|
626 | continue | |
|
627 | ||
|
628 | if line.modified: | |
|
629 | yield (None, line.modified.lineno, | |
|
630 | line.modified.action, line.modified.content) | |
|
631 | ||
|
632 | for b in buf: | |
|
633 | yield b | |
|
634 | ||
|
635 | return generator() |
@@ -180,6 +180,8 b' class Action(object):' | |||
|
180 | 180 | UNMODIFIED = 'unmod' |
|
181 | 181 | |
|
182 | 182 | CONTEXT = 'context' |
|
183 | CONTEXT_OLD = 'context-old' | |
|
184 | CONTEXT_NEW = 'context-new' | |
|
183 | 185 | |
|
184 | 186 | |
|
185 | 187 | class DiffProcessor(object): |
@@ -227,7 +229,7 b' class DiffProcessor(object):' | |||
|
227 | 229 | self._parser = self._parse_gitdiff |
|
228 | 230 | else: |
|
229 | 231 | self.differ = self._highlight_line_udiff |
|
230 |
self._parser = self._parse_ |
|
|
232 | self._parser = self._new_parse_gitdiff | |
|
231 | 233 | |
|
232 | 234 | def _copy_iterator(self): |
|
233 | 235 | """ |
@@ -491,9 +493,181 b' class DiffProcessor(object):' | |||
|
491 | 493 | |
|
492 | 494 | return diff_container(sorted(_files, key=sorter)) |
|
493 | 495 | |
|
494 | def _parse_udiff(self, inline_diff=True): | |
|
495 | raise NotImplementedError() | |
|
496 | ||
|
497 | # FIXME: NEWDIFFS: dan: this replaces the old _escaper function | |
|
498 | def _process_line(self, string): | |
|
499 | """ | |
|
500 | Process a diff line, checks the diff limit | |
|
501 | ||
|
502 | :param string: | |
|
503 | """ | |
|
504 | ||
|
505 | self.cur_diff_size += len(string) | |
|
506 | ||
|
507 | if not self.show_full_diff and (self.cur_diff_size > self.diff_limit): | |
|
508 | raise DiffLimitExceeded('Diff Limit Exceeded') | |
|
509 | ||
|
510 | return safe_unicode(string) | |
|
511 | ||
|
512 | # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff | |
|
513 | def _new_parse_gitdiff(self, inline_diff=True): | |
|
514 | _files = [] | |
|
515 | diff_container = lambda arg: arg | |
|
516 | for chunk in self._diff.chunks(): | |
|
517 | head = chunk.header | |
|
518 | log.debug('parsing diff %r' % head) | |
|
519 | ||
|
520 | diff = imap(self._process_line, chunk.diff.splitlines(1)) | |
|
521 | raw_diff = chunk.raw | |
|
522 | limited_diff = False | |
|
523 | exceeds_limit = False | |
|
524 | # if 'empty_file_to_modify_and_rename' in head['a_path']: | |
|
525 | # 1/0 | |
|
526 | op = None | |
|
527 | stats = { | |
|
528 | 'added': 0, | |
|
529 | 'deleted': 0, | |
|
530 | 'binary': False, | |
|
531 | 'old_mode': None, | |
|
532 | 'new_mode': None, | |
|
533 | 'ops': {}, | |
|
534 | } | |
|
535 | if head['old_mode']: | |
|
536 | stats['old_mode'] = head['old_mode'] | |
|
537 | if head['new_mode']: | |
|
538 | stats['new_mode'] = head['new_mode'] | |
|
539 | if head['b_mode']: | |
|
540 | stats['new_mode'] = head['b_mode'] | |
|
541 | ||
|
542 | if head['deleted_file_mode']: | |
|
543 | op = OPS.DEL | |
|
544 | stats['binary'] = True | |
|
545 | stats['ops'][DEL_FILENODE] = 'deleted file' | |
|
546 | ||
|
547 | elif head['new_file_mode']: | |
|
548 | op = OPS.ADD | |
|
549 | stats['binary'] = True | |
|
550 | stats['old_mode'] = None | |
|
551 | stats['new_mode'] = head['new_file_mode'] | |
|
552 | stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode'] | |
|
553 | else: # modify operation, can be copy, rename or chmod | |
|
554 | ||
|
555 | # CHMOD | |
|
556 | if head['new_mode'] and head['old_mode']: | |
|
557 | op = OPS.MOD | |
|
558 | stats['binary'] = True | |
|
559 | stats['ops'][CHMOD_FILENODE] = ( | |
|
560 | 'modified file chmod %s => %s' % ( | |
|
561 | head['old_mode'], head['new_mode'])) | |
|
562 | ||
|
563 | # RENAME | |
|
564 | if head['rename_from'] != head['rename_to']: | |
|
565 | op = OPS.MOD | |
|
566 | stats['binary'] = True | |
|
567 | stats['renamed'] = (head['rename_from'], head['rename_to']) | |
|
568 | stats['ops'][RENAMED_FILENODE] = ( | |
|
569 | 'file renamed from %s to %s' % ( | |
|
570 | head['rename_from'], head['rename_to'])) | |
|
571 | # COPY | |
|
572 | if head.get('copy_from') and head.get('copy_to'): | |
|
573 | op = OPS.MOD | |
|
574 | stats['binary'] = True | |
|
575 | stats['copied'] = (head['copy_from'], head['copy_to']) | |
|
576 | stats['ops'][COPIED_FILENODE] = ( | |
|
577 | 'file copied from %s to %s' % ( | |
|
578 | head['copy_from'], head['copy_to'])) | |
|
496 | 579 | |
|
580 | # If our new parsed headers didn't match anything fallback to | |
|
581 | # old style detection | |
|
582 | if op is None: | |
|
583 | if not head['a_file'] and head['b_file']: | |
|
584 | op = OPS.ADD | |
|
585 | stats['binary'] = True | |
|
586 | stats['new_file'] = True | |
|
587 | stats['ops'][NEW_FILENODE] = 'new file' | |
|
588 | ||
|
589 | elif head['a_file'] and not head['b_file']: | |
|
590 | op = OPS.DEL | |
|
591 | stats['binary'] = True | |
|
592 | stats['ops'][DEL_FILENODE] = 'deleted file' | |
|
593 | ||
|
594 | # it's not ADD not DELETE | |
|
595 | if op is None: | |
|
596 | op = OPS.MOD | |
|
597 | stats['binary'] = True | |
|
598 | stats['ops'][MOD_FILENODE] = 'modified file' | |
|
599 | ||
|
600 | # a real non-binary diff | |
|
601 | if head['a_file'] or head['b_file']: | |
|
602 | try: | |
|
603 | raw_diff, chunks, _stats = self._new_parse_lines(diff) | |
|
604 | stats['binary'] = False | |
|
605 | stats['added'] = _stats[0] | |
|
606 | stats['deleted'] = _stats[1] | |
|
607 | # explicit mark that it's a modified file | |
|
608 | if op == OPS.MOD: | |
|
609 | stats['ops'][MOD_FILENODE] = 'modified file' | |
|
610 | exceeds_limit = len(raw_diff) > self.file_limit | |
|
611 | ||
|
612 | # changed from _escaper function so we validate size of | |
|
613 | # each file instead of the whole diff | |
|
614 | # diff will hide big files but still show small ones | |
|
615 | # from my tests, big files are fairly safe to be parsed | |
|
616 | # but the browser is the bottleneck | |
|
617 | if not self.show_full_diff and exceeds_limit: | |
|
618 | raise DiffLimitExceeded('File Limit Exceeded') | |
|
619 | ||
|
620 | except DiffLimitExceeded: | |
|
621 | diff_container = lambda _diff: \ | |
|
622 | LimitedDiffContainer( | |
|
623 | self.diff_limit, self.cur_diff_size, _diff) | |
|
624 | ||
|
625 | exceeds_limit = len(raw_diff) > self.file_limit | |
|
626 | limited_diff = True | |
|
627 | chunks = [] | |
|
628 | ||
|
629 | else: # GIT format binary patch, or possibly empty diff | |
|
630 | if head['bin_patch']: | |
|
631 | # we have operation already extracted, but we mark simply | |
|
632 | # it's a diff we wont show for binary files | |
|
633 | stats['ops'][BIN_FILENODE] = 'binary diff hidden' | |
|
634 | chunks = [] | |
|
635 | ||
|
636 | if chunks and not self.show_full_diff and op == OPS.DEL: | |
|
637 | # if not full diff mode show deleted file contents | |
|
638 | # TODO: anderson: if the view is not too big, there is no way | |
|
639 | # to see the content of the file | |
|
640 | chunks = [] | |
|
641 | ||
|
642 | chunks.insert(0, [{ | |
|
643 | 'old_lineno': '', | |
|
644 | 'new_lineno': '', | |
|
645 | 'action': Action.CONTEXT, | |
|
646 | 'line': msg, | |
|
647 | } for _op, msg in stats['ops'].iteritems() | |
|
648 | if _op not in [MOD_FILENODE]]) | |
|
649 | ||
|
650 | original_filename = safe_unicode(head['a_path']) | |
|
651 | _files.append({ | |
|
652 | 'original_filename': original_filename, | |
|
653 | 'filename': safe_unicode(head['b_path']), | |
|
654 | 'old_revision': head['a_blob_id'], | |
|
655 | 'new_revision': head['b_blob_id'], | |
|
656 | 'chunks': chunks, | |
|
657 | 'raw_diff': safe_unicode(raw_diff), | |
|
658 | 'operation': op, | |
|
659 | 'stats': stats, | |
|
660 | 'exceeds_limit': exceeds_limit, | |
|
661 | 'is_limited_diff': limited_diff, | |
|
662 | }) | |
|
663 | ||
|
664 | ||
|
665 | sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1, | |
|
666 | OPS.DEL: 2}.get(info['operation']) | |
|
667 | ||
|
668 | return diff_container(sorted(_files, key=sorter)) | |
|
669 | ||
|
670 | # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines | |
|
497 | 671 | def _parse_lines(self, diff): |
|
498 | 672 | """ |
|
499 | 673 | Parse the diff an return data for the template. |
@@ -588,6 +762,107 b' class DiffProcessor(object):' | |||
|
588 | 762 | pass |
|
589 | 763 | return ''.join(raw_diff), chunks, stats |
|
590 | 764 | |
|
765 | # FIXME: NEWDIFFS: dan: this replaces _parse_lines | |
|
766 | def _new_parse_lines(self, diff): | |
|
767 | """ | |
|
768 | Parse the diff an return data for the template. | |
|
769 | """ | |
|
770 | ||
|
771 | lineiter = iter(diff) | |
|
772 | stats = [0, 0] | |
|
773 | chunks = [] | |
|
774 | raw_diff = [] | |
|
775 | ||
|
776 | try: | |
|
777 | line = lineiter.next() | |
|
778 | ||
|
779 | while line: | |
|
780 | raw_diff.append(line) | |
|
781 | match = self._chunk_re.match(line) | |
|
782 | ||
|
783 | if not match: | |
|
784 | break | |
|
785 | ||
|
786 | gr = match.groups() | |
|
787 | (old_line, old_end, | |
|
788 | new_line, new_end) = [int(x or 1) for x in gr[:-1]] | |
|
789 | ||
|
790 | lines = [] | |
|
791 | hunk = { | |
|
792 | 'section_header': gr[-1], | |
|
793 | 'source_start': old_line, | |
|
794 | 'source_length': old_end, | |
|
795 | 'target_start': new_line, | |
|
796 | 'target_length': new_end, | |
|
797 | 'lines': lines, | |
|
798 | } | |
|
799 | chunks.append(hunk) | |
|
800 | ||
|
801 | old_line -= 1 | |
|
802 | new_line -= 1 | |
|
803 | ||
|
804 | context = len(gr) == 5 | |
|
805 | old_end += old_line | |
|
806 | new_end += new_line | |
|
807 | ||
|
808 | line = lineiter.next() | |
|
809 | ||
|
810 | while old_line < old_end or new_line < new_end: | |
|
811 | command = ' ' | |
|
812 | if line: | |
|
813 | command = line[0] | |
|
814 | ||
|
815 | affects_old = affects_new = False | |
|
816 | ||
|
817 | # ignore those if we don't expect them | |
|
818 | if command in '#@': | |
|
819 | continue | |
|
820 | elif command == '+': | |
|
821 | affects_new = True | |
|
822 | action = Action.ADD | |
|
823 | stats[0] += 1 | |
|
824 | elif command == '-': | |
|
825 | affects_old = True | |
|
826 | action = Action.DELETE | |
|
827 | stats[1] += 1 | |
|
828 | else: | |
|
829 | affects_old = affects_new = True | |
|
830 | action = Action.UNMODIFIED | |
|
831 | ||
|
832 | if not self._newline_marker.match(line): | |
|
833 | old_line += affects_old | |
|
834 | new_line += affects_new | |
|
835 | lines.append({ | |
|
836 | 'old_lineno': affects_old and old_line or '', | |
|
837 | 'new_lineno': affects_new and new_line or '', | |
|
838 | 'action': action, | |
|
839 | 'line': self._clean_line(line, command) | |
|
840 | }) | |
|
841 | raw_diff.append(line) | |
|
842 | ||
|
843 | line = lineiter.next() | |
|
844 | ||
|
845 | if self._newline_marker.match(line): | |
|
846 | # we need to append to lines, since this is not | |
|
847 | # counted in the line specs of diff | |
|
848 | if affects_old: | |
|
849 | action = Action.CONTEXT_OLD | |
|
850 | elif affects_new: | |
|
851 | action = Action.CONTEXT_NEW | |
|
852 | else: | |
|
853 | raise Exception('invalid context for no newline') | |
|
854 | ||
|
855 | lines.append({ | |
|
856 | 'old_lineno': None, | |
|
857 | 'new_lineno': None, | |
|
858 | 'action': action, | |
|
859 | 'line': self._clean_line(line, command) | |
|
860 | }) | |
|
861 | ||
|
862 | except StopIteration: | |
|
863 | pass | |
|
864 | return ''.join(raw_diff), chunks, stats | |
|
865 | ||
|
591 | 866 | def _safe_id(self, idstring): |
|
592 | 867 | """Make a string safe for including in an id attribute. |
|
593 | 868 |
@@ -32,11 +32,13 b' class GitDiff(base.Diff):' | |||
|
32 | 32 | _header_re = re.compile(r""" |
|
33 | 33 | #^diff[ ]--git |
|
34 | 34 | [ ]"?a/(?P<a_path>.+?)"?[ ]"?b/(?P<b_path>.+?)"?\n |
|
35 | (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n | |
|
36 | ^rename[ ]from[ ](?P<rename_from>[^\r\n]+)\n | |
|
37 | ^rename[ ]to[ ](?P<rename_to>[^\r\n]+)(?:\n|$))? | |
|
38 | 35 | (?:^old[ ]mode[ ](?P<old_mode>\d+)\n |
|
39 | 36 | ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? |
|
37 | (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))? | |
|
38 | (?:^rename[ ]from[ ](?P<rename_from>[^\r\n]+)\n | |
|
39 | ^rename[ ]to[ ](?P<rename_to>[^\r\n]+)(?:\n|$))? | |
|
40 | (?:^copy[ ]from[ ](?P<copy_from>[^\r\n]+)\n | |
|
41 | ^copy[ ]to[ ](?P<copy_to>[^\r\n]+)(?:\n|$))? | |
|
40 | 42 | (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? |
|
41 | 43 | (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? |
|
42 | 44 | (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) |
@@ -188,6 +188,14 b' input[type="button"] {' | |||
|
188 | 188 | padding: @padding * 1.2; |
|
189 | 189 | } |
|
190 | 190 | |
|
191 | .btn-group { | |
|
192 | display: inline-block; | |
|
193 | .btn { | |
|
194 | float: left; | |
|
195 | margin: 0 0 0 -1px; | |
|
196 | } | |
|
197 | } | |
|
198 | ||
|
191 | 199 | .btn-link { |
|
192 | 200 | background: transparent; |
|
193 | 201 | border: none; |
@@ -326,15 +334,15 b' input[type="submit"],' | |||
|
326 | 334 | input[type="reset"] { |
|
327 | 335 | &.btn-danger { |
|
328 | 336 | &:extend(.btn-danger); |
|
329 | ||
|
337 | ||
|
330 | 338 | &:focus { |
|
331 | 339 | outline: 0; |
|
332 | 340 | } |
|
333 | ||
|
341 | ||
|
334 | 342 | &:hover { |
|
335 | 343 | &:extend(.btn-danger:hover); |
|
336 | 344 | } |
|
337 | ||
|
345 | ||
|
338 | 346 | &.btn-link { |
|
339 | 347 | &:extend(.btn-link); |
|
340 | 348 | color: @alert2; |
@@ -646,14 +646,210 b' pre.literal-block, .codehilite pre{' | |||
|
646 | 646 | |
|
647 | 647 | @cb-line-height: 18px; |
|
648 | 648 | @cb-line-code-padding: 10px; |
|
649 | @cb-text-padding: 5px; | |
|
649 | 650 | |
|
651 | @diff-pill-padding: 2px 7px; | |
|
652 | ||
|
653 | input.diff-collapse-state { | |
|
654 | display: none; | |
|
655 | ||
|
656 | &:checked + .diff { /* file diff is collapsed */ | |
|
657 | .cb { | |
|
658 | display: none | |
|
659 | } | |
|
660 | .diff-collapse-indicator { | |
|
661 | border-width: 9px 0 9px 15.6px; | |
|
662 | border-color: transparent transparent transparent #ccc; | |
|
663 | } | |
|
664 | .diff-menu { | |
|
665 | display: none; | |
|
666 | } | |
|
667 | margin: -1px 0 0 0; | |
|
668 | } | |
|
669 | ||
|
670 | &+ .diff { /* file diff is expanded */ | |
|
671 | .diff-collapse-indicator { | |
|
672 | border-width: 15.6px 9px 0 9px; | |
|
673 | border-color: #ccc transparent transparent transparent; | |
|
674 | } | |
|
675 | .diff-menu { | |
|
676 | display: block; | |
|
677 | } | |
|
678 | margin: 20px 0; | |
|
679 | } | |
|
680 | } | |
|
681 | .diff { | |
|
682 | border: 1px solid @grey5; | |
|
683 | ||
|
684 | /* START OVERRIDES */ | |
|
685 | .code-highlight { | |
|
686 | border: none; // TODO: remove this border from the global | |
|
687 | // .code-highlight, it doesn't belong there | |
|
688 | } | |
|
689 | label { | |
|
690 | margin: 0; // TODO: remove this margin definition from global label | |
|
691 | // it doesn't belong there - if margin on labels | |
|
692 | // are needed for a form they should be defined | |
|
693 | // in the form's class | |
|
694 | } | |
|
695 | /* END OVERRIDES */ | |
|
696 | ||
|
697 | * { | |
|
698 | box-sizing: border-box; | |
|
699 | } | |
|
700 | .diff-anchor { | |
|
701 | visibility: hidden; | |
|
702 | } | |
|
703 | &:hover { | |
|
704 | .diff-anchor { | |
|
705 | visibility: visible; | |
|
706 | } | |
|
707 | } | |
|
708 | ||
|
709 | .diff-collapse-indicator { | |
|
710 | width: 0; | |
|
711 | height: 0; | |
|
712 | border-style: solid; | |
|
713 | float: left; | |
|
714 | margin: 2px 2px 0 0; | |
|
715 | cursor: pointer; | |
|
716 | } | |
|
717 | ||
|
718 | .diff-heading { | |
|
719 | background: @grey7; | |
|
720 | cursor: pointer; | |
|
721 | display: block; | |
|
722 | padding: 5px 10px; | |
|
723 | } | |
|
724 | .diff-heading:after { | |
|
725 | content: ""; | |
|
726 | display: table; | |
|
727 | clear: both; | |
|
728 | } | |
|
729 | .diff-heading:hover { | |
|
730 | background: #e1e9f4 !important; | |
|
731 | } | |
|
732 | ||
|
733 | .diff-menu { | |
|
734 | float: right; | |
|
735 | a, button { | |
|
736 | padding: 5px; | |
|
737 | display: block; | |
|
738 | float: left | |
|
739 | } | |
|
740 | } | |
|
741 | .diff-pill { | |
|
742 | display: block; | |
|
743 | float: left; | |
|
744 | padding: @diff-pill-padding; | |
|
745 | } | |
|
746 | .diff-pill-group { | |
|
747 | .diff-pill { | |
|
748 | opacity: .8; | |
|
749 | &:first-child { | |
|
750 | border-radius: @border-radius 0 0 @border-radius; | |
|
751 | } | |
|
752 | &:last-child { | |
|
753 | border-radius: 0 @border-radius @border-radius 0; | |
|
754 | } | |
|
755 | &:only-child { | |
|
756 | border-radius: @border-radius; | |
|
757 | } | |
|
758 | } | |
|
759 | } | |
|
760 | .diff-pill { | |
|
761 | &[op="name"] { | |
|
762 | background: none; | |
|
763 | color: @grey2; | |
|
764 | opacity: 1; | |
|
765 | color: white; | |
|
766 | } | |
|
767 | &[op="limited"] { | |
|
768 | background: @grey2; | |
|
769 | color: white; | |
|
770 | } | |
|
771 | &[op="binary"] { | |
|
772 | background: @color7; | |
|
773 | color: white; | |
|
774 | } | |
|
775 | &[op="modified"] { | |
|
776 | background: @alert1; | |
|
777 | color: white; | |
|
778 | } | |
|
779 | &[op="renamed"] { | |
|
780 | background: @color4; | |
|
781 | color: white; | |
|
782 | } | |
|
783 | &[op="mode"] { | |
|
784 | background: @grey3; | |
|
785 | color: white; | |
|
786 | } | |
|
787 | &[op="symlink"] { | |
|
788 | background: @color8; | |
|
789 | color: white; | |
|
790 | } | |
|
791 | ||
|
792 | &[op="added"] { /* added lines */ | |
|
793 | background: @alert1; | |
|
794 | color: white; | |
|
795 | } | |
|
796 | &[op="deleted"] { /* deleted lines */ | |
|
797 | background: @alert2; | |
|
798 | color: white; | |
|
799 | } | |
|
800 | ||
|
801 | &[op="created"] { /* created file */ | |
|
802 | background: @alert1; | |
|
803 | color: white; | |
|
804 | } | |
|
805 | &[op="removed"] { /* deleted file */ | |
|
806 | background: @color5; | |
|
807 | color: white; | |
|
808 | } | |
|
809 | } | |
|
810 | ||
|
811 | .diff-collapse-button, .diff-expand-button { | |
|
812 | cursor: pointer; | |
|
813 | } | |
|
814 | .diff-collapse-button { | |
|
815 | display: inline; | |
|
816 | } | |
|
817 | .diff-expand-button { | |
|
818 | display: none; | |
|
819 | } | |
|
820 | .diff-collapsed .diff-collapse-button { | |
|
821 | display: none; | |
|
822 | } | |
|
823 | .diff-collapsed .diff-expand-button { | |
|
824 | display: inline; | |
|
825 | } | |
|
826 | } | |
|
650 | 827 | table.cb { |
|
651 | 828 | width: 100%; |
|
652 | 829 | border-collapse: collapse; |
|
653 | margin-bottom: 10px; | |
|
654 | 830 | |
|
655 | * { | |
|
656 | box-sizing: border-box; | |
|
831 | .cb-text { | |
|
832 | padding: @cb-text-padding; | |
|
833 | } | |
|
834 | .cb-hunk { | |
|
835 | padding: @cb-text-padding; | |
|
836 | } | |
|
837 | .cb-expand { | |
|
838 | display: none; | |
|
839 | } | |
|
840 | .cb-collapse { | |
|
841 | display: inline; | |
|
842 | } | |
|
843 | &.cb-collapsed { | |
|
844 | .cb-line { | |
|
845 | display: none; | |
|
846 | } | |
|
847 | .cb-expand { | |
|
848 | display: inline; | |
|
849 | } | |
|
850 | .cb-collapse { | |
|
851 | display: none; | |
|
852 | } | |
|
657 | 853 | } |
|
658 | 854 | |
|
659 | 855 | /* intentionally general selector since .cb-line-selected must override it |
@@ -663,18 +859,45 b' table.cb {' | |||
|
663 | 859 | .cb-line-fresh .cb-content { |
|
664 | 860 | background: white !important; |
|
665 | 861 | } |
|
862 | .cb-warning { | |
|
863 | background: #fff4dd; | |
|
864 | } | |
|
666 | 865 | |
|
667 | tr.cb-annotate { | |
|
668 | border-top: 1px solid #eee; | |
|
866 | &.cb-diff-sideside { | |
|
867 | td { | |
|
868 | &.cb-content { | |
|
869 | width: 50%; | |
|
870 | } | |
|
871 | } | |
|
872 | } | |
|
669 | 873 | |
|
670 | &+ .cb-line { | |
|
874 | tr { | |
|
875 | &.cb-annotate { | |
|
671 | 876 | border-top: 1px solid #eee; |
|
877 | ||
|
878 | &+ .cb-line { | |
|
879 | border-top: 1px solid #eee; | |
|
880 | } | |
|
881 | ||
|
882 | &:first-child { | |
|
883 | border-top: none; | |
|
884 | &+ .cb-line { | |
|
885 | border-top: none; | |
|
886 | } | |
|
887 | } | |
|
672 | 888 | } |
|
673 | 889 | |
|
674 |
& |
|
|
675 | border-top: none; | |
|
676 | &+ .cb-line { | |
|
677 | border-top: none; | |
|
890 | &.cb-hunk { | |
|
891 | font-family: @font-family-monospace; | |
|
892 | color: rgba(0, 0, 0, 0.3); | |
|
893 | ||
|
894 | td { | |
|
895 | &:first-child { | |
|
896 | background: #edf2f9; | |
|
897 | } | |
|
898 | &:last-child { | |
|
899 | background: #f4f7fb; | |
|
900 | } | |
|
678 | 901 | } |
|
679 | 902 | } |
|
680 | 903 | } |
@@ -686,9 +909,14 b' table.cb {' | |||
|
686 | 909 | &.cb-content { |
|
687 | 910 | font-size: 12.35px; |
|
688 | 911 | |
|
912 | &.cb-line-selected .cb-code { | |
|
913 | background: @comment-highlight-color !important; | |
|
914 | } | |
|
915 | ||
|
689 | 916 | span.cb-code { |
|
690 | 917 | line-height: @cb-line-height; |
|
691 | 918 | padding-left: @cb-line-code-padding; |
|
919 | padding-right: @cb-line-code-padding; | |
|
692 | 920 | display: block; |
|
693 | 921 | white-space: pre-wrap; |
|
694 | 922 | font-family: @font-family-monospace; |
@@ -714,14 +942,38 b' table.cb {' | |||
|
714 | 942 | a { |
|
715 | 943 | display: block; |
|
716 | 944 | padding-right: @cb-line-code-padding; |
|
945 | padding-left: @cb-line-code-padding; | |
|
717 | 946 | line-height: @cb-line-height; |
|
718 | 947 | color: rgba(0, 0, 0, 0.3); |
|
719 | 948 | } |
|
720 | 949 | } |
|
721 | 950 | |
|
722 |
&.cb- |
|
|
723 | &.cb-line-selected .cb-code { | |
|
724 | background: @comment-highlight-color !important; | |
|
951 | &.cb-empty { | |
|
952 | background: @grey7; | |
|
953 | } | |
|
954 | ||
|
955 | ins { | |
|
956 | color: black; | |
|
957 | background: #a6f3a6; | |
|
958 | text-decoration: none; | |
|
959 | } | |
|
960 | del { | |
|
961 | color: black; | |
|
962 | background: #f8cbcb; | |
|
963 | text-decoration: none; | |
|
964 | } | |
|
965 | &.cb-addition { | |
|
966 | background: #ecffec; | |
|
967 | ||
|
968 | &.blob-lineno { | |
|
969 | background: #ddffdd; | |
|
970 | } | |
|
971 | } | |
|
972 | &.cb-deletion { | |
|
973 | background: #ffecec; | |
|
974 | ||
|
975 | &.blob-lineno { | |
|
976 | background: #ffdddd; | |
|
725 | 977 | } |
|
726 | 978 | } |
|
727 | 979 |
@@ -221,14 +221,32 b' var formatSelect2SelectionRefs = functio' | |||
|
221 | 221 | }; |
|
222 | 222 | |
|
223 | 223 | // takes a given html element and scrolls it down offset pixels |
|
224 | function offsetScroll(element, offset){ | |
|
225 | setTimeout(function(){ | |
|
224 | function offsetScroll(element, offset) { | |
|
225 | setTimeout(function() { | |
|
226 | 226 | var location = element.offset().top; |
|
227 | 227 | // some browsers use body, some use html |
|
228 | 228 | $('html, body').animate({ scrollTop: (location - offset) }); |
|
229 | 229 | }, 100); |
|
230 | 230 | } |
|
231 | 231 | |
|
232 | // scroll an element `percent`% from the top of page in `time` ms | |
|
233 | function scrollToElement(element, percent, time) { | |
|
234 | percent = (percent === undefined ? 25 : percent); | |
|
235 | time = (time === undefined ? 100 : time); | |
|
236 | ||
|
237 | var $element = $(element); | |
|
238 | var elOffset = $element.offset().top; | |
|
239 | var elHeight = $element.height(); | |
|
240 | var windowHeight = $(window).height(); | |
|
241 | var offset = elOffset; | |
|
242 | if (elHeight < windowHeight) { | |
|
243 | offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2)); | |
|
244 | } | |
|
245 | setTimeout(function() { | |
|
246 | $('html, body').animate({ scrollTop: offset}); | |
|
247 | }, time); | |
|
248 | } | |
|
249 | ||
|
232 | 250 | /** |
|
233 | 251 | * global hooks after DOM is loaded |
|
234 | 252 | */ |
@@ -418,6 +436,10 b' function offsetScroll(element, offset){' | |||
|
418 | 436 | var result = splitDelimitedHash(location.hash); |
|
419 | 437 | var loc = result.loc; |
|
420 | 438 | if (loc.length > 1) { |
|
439 | ||
|
440 | var highlightable_line_tds = []; | |
|
441 | ||
|
442 | // source code line format | |
|
421 | 443 | var page_highlights = loc.substring( |
|
422 | 444 | loc.indexOf('#') + 1).split('L'); |
|
423 | 445 | |
@@ -442,33 +464,27 b' function offsetScroll(element, offset){' | |||
|
442 | 464 | for (pos in h_lines) { |
|
443 | 465 | var line_td = $('td.cb-lineno#L' + h_lines[pos]); |
|
444 | 466 | if (line_td.length) { |
|
445 |
line_td |
|
|
446 | line_td.next().addClass('cb-line-selected'); // line content | |
|
467 | highlightable_line_tds.push(line_td); | |
|
447 | 468 | } |
|
448 | 469 | } |
|
449 | var first_line_td = $('td.cb-lineno#L' + h_lines[0]); | |
|
450 | if (first_line_td.length) { | |
|
451 | var elOffset = first_line_td.offset().top; | |
|
452 | var elHeight = first_line_td.height(); | |
|
453 | var windowHeight = $(window).height(); | |
|
454 | var offset; | |
|
470 | } | |
|
455 | 471 | |
|
456 | if (elHeight < windowHeight) { | |
|
457 | offset = elOffset - ((windowHeight / 4) - (elHeight / 2)); | |
|
458 | } | |
|
459 |
|
|
|
460 | offset = elOffset; | |
|
461 | } | |
|
462 | $(function() { // let browser scroll to hash first, then | |
|
463 | // scroll the line to the middle of page | |
|
464 | setTimeout(function() { | |
|
465 | $('html, body').animate({ scrollTop: offset }); | |
|
466 | }, 100); | |
|
467 | }); | |
|
468 |
|
|
|
469 |
|
|
|
470 |
|
|
|
471 | } | |
|
472 | // now check a direct id reference (diff page) | |
|
473 | if ($(loc).length && $(loc).hasClass('cb-lineno')) { | |
|
474 | highlightable_line_tds.push($(loc)); | |
|
475 | } | |
|
476 | $.each(highlightable_line_tds, function (i, $td) { | |
|
477 | $td.addClass('cb-line-selected'); // line number td | |
|
478 | $td.next().addClass('cb-line-selected'); // line content | |
|
479 | }); | |
|
480 | ||
|
481 | if (highlightable_line_tds.length) { | |
|
482 | var $first_line_td = highlightable_line_tds[0]; | |
|
483 | scrollToElement($first_line_td); | |
|
484 | $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({ | |
|
485 | lineno: $first_line_td, | |
|
486 | remainder: result.remainder | |
|
487 | }); | |
|
472 | 488 | } |
|
473 | 489 | } |
|
474 | 490 | } |
@@ -1,5 +1,6 b'' | |||
|
1 | 1 | ## -*- coding: utf-8 -*- |
|
2 | 2 | <%inherit file="/base/base.html"/> |
|
3 | <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/> | |
|
3 | 4 | |
|
4 | 5 | <%def name="title()"> |
|
5 | 6 | %if c.compare_home: |
@@ -53,7 +54,7 b'' | |||
|
53 | 54 | <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}"><i class="icon-refresh"></i> ${_('Swap')}</a> |
|
54 | 55 | %endif |
|
55 | 56 | <div id="compare_revs" class="btn btn-primary"><i class ="icon-loop"></i> ${_('Compare Commits')}</div> |
|
56 | %if c.files: | |
|
57 | %if c.diffset and c.diffset.files: | |
|
57 | 58 | <div id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</div> |
|
58 | 59 | %endif |
|
59 | 60 | </div> |
@@ -248,72 +249,7 b'' | |||
|
248 | 249 | <div id="changeset_compare_view_content"> |
|
249 | 250 | ##CS |
|
250 | 251 | <%include file="compare_commits.html"/> |
|
251 | ||
|
252 | ## FILES | |
|
253 | <div class="cs_files_title"> | |
|
254 | <span class="cs_files_expand"> | |
|
255 | <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span> | |
|
256 | </span> | |
|
257 | <h2> | |
|
258 | ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)} | |
|
259 | </h2> | |
|
260 | </div> | |
|
261 | <div class="cs_files"> | |
|
262 | %if not c.files: | |
|
263 | <p class="empty_data">${_('No files')}</p> | |
|
264 | %endif | |
|
265 | <table class="compare_view_files"> | |
|
266 | <%namespace name="diff_block" file="/changeset/diff_block.html"/> | |
|
267 | %for FID, change, path, stats, file in c.files: | |
|
268 | <tr class="cs_${change} collapse_file" fid="${FID}"> | |
|
269 | <td class="cs_icon_td"> | |
|
270 | <span class="collapse_file_icon" fid="${FID}"></span> | |
|
271 | </td> | |
|
272 | <td class="cs_icon_td"> | |
|
273 | <div class="flag_status not_reviewed hidden"></div> | |
|
274 | </td> | |
|
275 | <td class="cs_${change}" id="a_${FID}"> | |
|
276 | <div class="node"> | |
|
277 | <a href="#a_${FID}"> | |
|
278 | <i class="icon-file-${change.lower()}"></i> | |
|
279 | ${h.safe_unicode(path)} | |
|
280 | </a> | |
|
281 | </div> | |
|
282 | </td> | |
|
283 | <td> | |
|
284 | <div class="changes pull-right">${h.fancy_file_stats(stats)}</div> | |
|
285 | <div class="comment-bubble pull-right" data-path="${path}"> | |
|
286 | <i class="icon-comment"></i> | |
|
287 | </div> | |
|
288 | </td> | |
|
289 | </tr> | |
|
290 | <tr fid="${FID}" id="diff_${FID}" class="diff_links"> | |
|
291 | <td></td> | |
|
292 | <td></td> | |
|
293 | <td class="cs_${change}"> | |
|
294 | %if c.target_repo.repo_name == c.repo_name: | |
|
295 | ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.source_ref, c.target_ref, change, file)} | |
|
296 | %else: | |
|
297 | ## this is slightly different case later, since the target repo can have this | |
|
298 | ## file in target state than the source repo | |
|
299 | ${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.source_ref, c.target_ref, change, file)} | |
|
300 | %endif | |
|
301 | </td> | |
|
302 | <td class="td-actions rc-form"> | |
|
303 | </td> | |
|
304 | </tr> | |
|
305 | <tr id="tr_${FID}"> | |
|
306 | <td></td> | |
|
307 | <td></td> | |
|
308 | <td class="injected_diff" colspan="2"> | |
|
309 | ${diff_block.diff_block_simple([c.changes[FID]])} | |
|
310 | </td> | |
|
311 | </tr> | |
|
312 | %endfor | |
|
313 | </table> | |
|
314 | % if c.limited_diff: | |
|
315 | ${diff_block.changeset_message()} | |
|
316 | % endif | |
|
252 | ${cbdiffs.render_diffset(c.diffset)} | |
|
317 | 253 | </div> |
|
318 | 254 | %endif |
|
319 | 255 | </div> |
@@ -104,7 +104,7 b' class TestChangesetController(object):' | |||
|
104 | 104 | |
|
105 | 105 | response.mustcontain(_shorten_commit_id(commit_ids[0])) |
|
106 | 106 | response.mustcontain(_shorten_commit_id(commit_ids[1])) |
|
107 | ||
|
107 | ||
|
108 | 108 | # svn is special |
|
109 | 109 | if backend.alias == 'svn': |
|
110 | 110 | response.mustcontain('new file 10644') |
@@ -146,7 +146,7 b' class TestChangesetController(object):' | |||
|
146 | 146 | # files op files |
|
147 | 147 | response.mustcontain('File no longer present at commit: %s' % |
|
148 | 148 | _shorten_commit_id(commit_ids[1])) |
|
149 | ||
|
149 | ||
|
150 | 150 | # svn is special |
|
151 | 151 | if backend.alias == 'svn': |
|
152 | 152 | response.mustcontain('new file 10644') |
@@ -158,7 +158,7 b' class TestChangesetController(object):' | |||
|
158 | 158 | response.mustcontain('Added docstrings to vcs.cli') # commit msg |
|
159 | 159 | response.mustcontain('Changed theme to ADC theme') # commit msg |
|
160 | 160 | |
|
161 | self._check_diff_menus(response) | |
|
161 | self._check_new_diff_menus(response) | |
|
162 | 162 | |
|
163 | 163 | def test_changeset_range(self, backend): |
|
164 | 164 | self._check_changeset_range( |
@@ -273,7 +273,7 b' Added a symlink' | |||
|
273 | 273 | """ + diffs['svn'], |
|
274 | 274 | } |
|
275 | 275 | |
|
276 | def _check_diff_menus(self, response, right_menu=False): | |
|
276 | def _check_diff_menus(self, response, right_menu=False,): | |
|
277 | 277 | # diff menus |
|
278 | 278 | for elem in ['Show File', 'Unified Diff', 'Side-by-side Diff', |
|
279 | 279 | 'Raw Diff', 'Download Diff']: |
@@ -284,3 +284,16 b' Added a symlink' | |||
|
284 | 284 | for elem in ['Ignore whitespace', 'Increase context', |
|
285 | 285 | 'Hide comments']: |
|
286 | 286 | response.mustcontain(elem) |
|
287 | ||
|
288 | ||
|
289 | def _check_new_diff_menus(self, response, right_menu=False,): | |
|
290 | # diff menus | |
|
291 | for elem in ['Show file before', 'Show file after', | |
|
292 | 'Raw diff', 'Download diff']: | |
|
293 | response.mustcontain(elem) | |
|
294 | ||
|
295 | # right pane diff menus | |
|
296 | if right_menu: | |
|
297 | for elem in ['Ignore whitespace', 'Increase context', | |
|
298 | 'Hide comments']: | |
|
299 | response.mustcontain(elem) |
@@ -20,6 +20,7 b'' | |||
|
20 | 20 | |
|
21 | 21 | import mock |
|
22 | 22 | import pytest |
|
23 | import lxml.html | |
|
23 | 24 | |
|
24 | 25 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
25 | 26 | from rhodecode.lib.vcs.exceptions import RepositoryRequirementError |
@@ -609,9 +610,12 b' class ComparePage(AssertResponse):' | |||
|
609 | 610 | """ |
|
610 | 611 | |
|
611 | 612 | def contains_file_links_and_anchors(self, files): |
|
613 | doc = lxml.html.fromstring(self.response.body) | |
|
612 | 614 | for filename, file_id in files: |
|
613 | self.contains_one_link(filename, '#' + file_id) | |
|
614 | 615 | self.contains_one_anchor(file_id) |
|
616 | diffblock = doc.cssselect('[data-f-path="%s"]' % filename) | |
|
617 | assert len(diffblock) == 1 | |
|
618 | assert len(diffblock[0].cssselect('a[href="#%s"]' % file_id)) == 1 | |
|
615 | 619 | |
|
616 | 620 | def contains_change_summary(self, files_changed, inserted, deleted): |
|
617 | 621 | template = ( |
@@ -264,19 +264,19 b' class TestRenderTokenStream(object):' | |||
|
264 | 264 | ), |
|
265 | 265 | ( |
|
266 | 266 | [('A', '', u'two\n'), ('A', '', u'lines')], |
|
267 |
'<span class="A">two |
|
|
267 | '<span class="A">two\nlines</span>', | |
|
268 | 268 | ), |
|
269 | 269 | ( |
|
270 | 270 | [('A', '', u'\nthree\n'), ('A', '', u'lines')], |
|
271 |
'<span class="A"> |
|
|
271 | '<span class="A">\nthree\nlines</span>', | |
|
272 | 272 | ), |
|
273 | 273 | ( |
|
274 | 274 | [('', '', u'\n'), ('A', '', u'line')], |
|
275 |
'<span> |
|
|
275 | '<span>\n</span><span class="A">line</span>', | |
|
276 | 276 | ), |
|
277 | 277 | ( |
|
278 | 278 | [('', 'ins', u'\n'), ('A', '', u'line')], |
|
279 |
'<span><ins> |
|
|
279 | '<span><ins>\n</ins></span><span class="A">line</span>', | |
|
280 | 280 | ), |
|
281 | 281 | ( |
|
282 | 282 | [('A', '', u'hel'), ('A', 'ins', u'lo')], |
General Comments 0
You need to be logged in to leave comments.
Login now