1 from __future__
import absolute_import
, division
, print_function
, unicode_literals
4 from collections
import defaultdict
9 _HUNK_HEADER_RE
= re
.compile(r
'^@@ -([0-9,]+) \+([0-9,]+) @@(.*)')
12 class _DiffHunk(object):
14 self
, old_start
, old_count
, new_start
, new_count
, heading
, first_line_idx
, lines
16 self
.old_start
= old_start
17 self
.old_count
= old_count
18 self
.new_start
= new_start
19 self
.new_count
= new_count
20 self
.heading
= heading
21 self
.first_line_idx
= first_line_idx
25 def last_line_idx(self
):
26 return self
.first_line_idx
+ len(self
.lines
) - 1
29 def parse_range_str(range_str
):
31 begin
, end
= range_str
.split(',', 1)
32 return int(begin
), int(end
)
33 return int(range_str
), 1
36 def _format_range(start
, count
):
39 return '%d,%d' % (start
, count
)
42 def _format_hunk_header(old_start
, old_count
, new_start
, new_count
, heading
=''):
43 return '@@ -%s +%s @@%s\n' % (
44 _format_range(old_start
, old_count
),
45 _format_range(new_start
, new_count
),
50 def _parse_diff(diff_text
):
52 for line_idx
, line
in enumerate(diff_text
.split('\n')):
53 match
= _HUNK_HEADER_RE
.match(line
)
55 old_start
, old_count
= parse_range_str(match
.group(1))
56 new_start
, new_count
= parse_range_str(match
.group(2))
57 heading
= match
.group(3)
70 hunks
[-1].lines
.append(line
+ '\n')
75 """Return the number of digits needed to display a number"""
77 result
= int(math
.log10(number
)) + 1
83 class Counter(object):
84 """Keep track of a diff range's values"""
86 def __init__(self
, value
=0, max_value
=-1):
88 self
.max_value
= max_value
89 self
._initial
_max
_value
= max_value
92 """Reset the max counter and return self for convenience"""
93 self
.max_value
= self
._initial
_max
_value
96 def parse(self
, range_str
):
97 """Parse a diff range and setup internal state"""
98 start
, count
= parse_range_str(range_str
)
100 self
.max_value
= max(start
+ count
- 1, self
.max_value
)
102 def tick(self
, amount
=1):
103 """Return the current value and increment to the next"""
109 class DiffLines(object):
110 """Parse diffs and gather line numbers"""
119 # merge <ours> <theirs> <new>
122 self
.ours
= Counter()
123 self
.theirs
= Counter()
131 self
.theirs
.max_value
,
135 def parse(self
, diff_text
):
138 state
= INITIAL_STATE
= 0
139 merge
= self
.merge
= False
140 NO_NEWLINE
= r
'\ No newline at end of file'
142 old
= self
.old
.reset()
143 new
= self
.new
.reset()
144 ours
= self
.ours
.reset()
145 theirs
= self
.theirs
.reset()
147 for text
in diff_text
.split('\n'):
148 if text
.startswith('@@ -'):
149 parts
= text
.split(' ', 4)
150 if parts
[0] == '@@' and parts
[3] == '@@':
152 old
.parse(parts
[1][1:])
153 new
.parse(parts
[2][1:])
154 lines
.append((self
.DASH
, self
.DASH
))
156 if text
.startswith('@@@ -'):
157 self
.merge
= merge
= True
158 parts
= text
.split(' ', 5)
159 if parts
[0] == '@@@' and parts
[4] == '@@@':
161 ours
.parse(parts
[1][1:])
162 theirs
.parse(parts
[2][1:])
163 new
.parse(parts
[3][1:])
164 lines
.append((self
.DASH
, self
.DASH
, self
.DASH
))
166 if state
== INITIAL_STATE
or text
.rstrip() == NO_NEWLINE
:
168 lines
.append((self
.EMPTY
, self
.EMPTY
, self
.EMPTY
))
170 lines
.append((self
.EMPTY
, self
.EMPTY
))
171 elif not merge
and text
.startswith('-'):
172 lines
.append((old
.tick(), self
.EMPTY
))
173 elif merge
and text
.startswith('- '):
174 lines
.append((ours
.tick(), self
.EMPTY
, self
.EMPTY
))
175 elif merge
and text
.startswith(' -'):
176 lines
.append((self
.EMPTY
, theirs
.tick(), self
.EMPTY
))
177 elif merge
and text
.startswith('--'):
178 lines
.append((ours
.tick(), theirs
.tick(), self
.EMPTY
))
179 elif not merge
and text
.startswith('+'):
180 lines
.append((self
.EMPTY
, new
.tick()))
181 elif merge
and text
.startswith('++'):
182 lines
.append((self
.EMPTY
, self
.EMPTY
, new
.tick()))
183 elif merge
and text
.startswith('+ '):
184 lines
.append((self
.EMPTY
, theirs
.tick(), new
.tick()))
185 elif merge
and text
.startswith(' +'):
186 lines
.append((ours
.tick(), self
.EMPTY
, new
.tick()))
187 elif not merge
and text
.startswith(' '):
188 lines
.append((old
.tick(), new
.tick()))
189 elif merge
and text
.startswith(' '):
190 lines
.append((ours
.tick(), theirs
.tick(), new
.tick()))
197 state
= INITIAL_STATE
199 lines
.append((self
.EMPTY
, self
.EMPTY
, self
.EMPTY
))
201 lines
.append((self
.EMPTY
, self
.EMPTY
))
206 class FormatDigits(object):
207 """Format numbers for use in diff line numbers"""
209 DASH
= DiffLines
.DASH
210 EMPTY
= DiffLines
.EMPTY
212 def __init__(self
, dash
='', empty
=''):
216 self
._dash
= dash
or compat
.uchr(0xB7)
217 self
._empty
= empty
or ' '
219 def set_digits(self
, value
):
220 self
.fmt
= '%%0%dd' % value
221 self
.empty
= self
._empty
* value
222 self
.dash
= self
._dash
* value
224 def value(self
, old
, new
):
225 old_str
= self
._format
(old
)
226 new_str
= self
._format
(new
)
227 return '%s %s' % (old_str
, new_str
)
229 def merge_value(self
, old
, base
, new
):
230 old_str
= self
._format
(old
)
231 base_str
= self
._format
(base
)
232 new_str
= self
._format
(new
)
233 return '%s %s %s' % (old_str
, base_str
, new_str
)
235 def number(self
, value
):
236 return self
.fmt
% value
238 def _format(self
, value
):
239 if value
== self
.DASH
:
241 elif value
== self
.EMPTY
:
244 result
= self
.number(value
)
248 class DiffParser(object):
249 """Parse and rewrite diffs to produce edited patches
251 This parser is used for modifying the worktree and index by constructing
252 temporary patches that are applied using "git apply".
256 def __init__(self
, filename
, diff_text
):
257 self
.filename
= filename
258 self
.hunks
= _parse_diff(diff_text
)
260 def generate_patch(self
, first_line_idx
, last_line_idx
, reverse
=False):
261 """Return a patch containing a subset of the diff"""
268 lines
= ['--- a/%s\n' % self
.filename
, '+++ b/%s\n' % self
.filename
]
272 for hunk
in self
.hunks
:
273 # skip hunks until we get to the one that contains the first
275 if hunk
.last_line_idx
< first_line_idx
:
277 # once we have processed the hunk that contains the last selected
279 if hunk
.first_line_idx
> last_line_idx
:
283 counts
= defaultdict(int)
286 for line_idx
, line
in enumerate(
287 hunk
.lines
[1:], start
=hunk
.first_line_idx
+ 1
289 line_type
, line_content
= line
[:1], line
[1:]
292 if line_type
== ADDITION
:
294 elif line_type
== DELETION
:
297 if not first_line_idx
<= line_idx
<= last_line_idx
:
298 if line_type
== ADDITION
:
299 # Skip additions that are not selected.
302 if line_type
== DELETION
:
303 # Change deletions that are not selected to context.
305 if line_type
== NO_NEWLINE
and prev_skipped
:
306 # If the line immediately before a "No newline" line was
307 # skipped (because it was an unselected addition) skip
308 # the "No newline" line as well.
310 filtered_lines
.append(line_type
+ line_content
)
311 counts
[line_type
] += 1
314 # Do not include hunks that, after filtering, have only context
315 # lines (no additions or deletions).
316 if not counts
[ADDITION
] and not counts
[DELETION
]:
319 old_count
= counts
[CONTEXT
] + counts
[DELETION
]
320 new_count
= counts
[CONTEXT
] + counts
[ADDITION
]
323 old_start
= hunk
.new_start
325 old_start
= hunk
.old_start
326 new_start
= old_start
+ start_offset
332 start_offset
+= counts
[ADDITION
] - counts
[DELETION
]
336 old_start
, old_count
, new_start
, new_count
, hunk
.heading
339 lines
.extend(filtered_lines
)
341 # If there are only two lines, that means we did not include any hunks,
345 return ''.join(lines
)
347 def generate_hunk_patch(self
, line_idx
, reverse
=False):
348 """Return a patch containing the hunk for the specified line only"""
350 for hunk
in self
.hunks
:
351 if line_idx
<= hunk
.last_line_idx
:
355 return self
.generate_patch(
356 hunk
.first_line_idx
, hunk
.last_line_idx
, reverse
=reverse