1 from __future__
import division
, absolute_import
, 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
, 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"""
120 # merge <ours> <theirs> <new>
123 self
.ours
= Counter()
124 self
.theirs
= Counter()
132 self
.theirs
.max_value
,
136 def parse(self
, diff_text
):
139 state
= INITIAL_STATE
= 0
140 merge
= self
.merge
= False
141 NO_NEWLINE
= r
'\ No newline at end of file'
143 old
= self
.old
.reset()
144 new
= self
.new
.reset()
145 ours
= self
.ours
.reset()
146 theirs
= self
.theirs
.reset()
148 for text
in diff_text
.split('\n'):
149 if text
.startswith('@@ -'):
150 parts
= text
.split(' ', 4)
151 if parts
[0] == '@@' and parts
[3] == '@@':
153 old
.parse(parts
[1][1:])
154 new
.parse(parts
[2][1:])
155 lines
.append((self
.DASH
, self
.DASH
))
157 if text
.startswith('@@@ -'):
158 self
.merge
= merge
= True
159 parts
= text
.split(' ', 5)
160 if parts
[0] == '@@@' and parts
[4] == '@@@':
162 ours
.parse(parts
[1][1:])
163 theirs
.parse(parts
[2][1:])
164 new
.parse(parts
[3][1:])
165 lines
.append((self
.DASH
, self
.DASH
, self
.DASH
))
167 if state
== INITIAL_STATE
or text
.rstrip() == NO_NEWLINE
:
169 lines
.append((self
.EMPTY
, self
.EMPTY
, self
.EMPTY
))
171 lines
.append((self
.EMPTY
, self
.EMPTY
))
172 elif not merge
and text
.startswith('-'):
173 lines
.append((old
.tick(), self
.EMPTY
))
174 elif merge
and text
.startswith('- '):
175 lines
.append((self
.EMPTY
, theirs
.tick(), self
.EMPTY
))
176 elif merge
and text
.startswith(' -'):
177 lines
.append((self
.EMPTY
, theirs
.tick(), self
.EMPTY
))
178 elif merge
and text
.startswith('--'):
179 lines
.append((ours
.tick(), theirs
.tick(), self
.EMPTY
))
180 elif not merge
and text
.startswith('+'):
181 lines
.append((self
.EMPTY
, new
.tick()))
182 elif merge
and text
.startswith('++'):
183 lines
.append((self
.EMPTY
, self
.EMPTY
, new
.tick()))
184 elif merge
and text
.startswith('+ '):
185 lines
.append((self
.EMPTY
, theirs
.tick(), new
.tick()))
186 elif merge
and text
.startswith(' +'):
187 lines
.append((ours
.tick(), self
.EMPTY
, new
.tick()))
188 elif not merge
and text
.startswith(' '):
189 lines
.append((old
.tick(), new
.tick()))
190 elif merge
and text
.startswith(' '):
191 lines
.append((ours
.tick(), theirs
.tick(), new
.tick()))
198 state
= INITIAL_STATE
200 lines
.append((self
.EMPTY
, self
.EMPTY
, self
.EMPTY
))
202 lines
.append((self
.EMPTY
, self
.EMPTY
))
207 class FormatDigits(object):
208 """Format numbers for use in diff line numbers"""
210 DASH
= DiffLines
.DASH
211 EMPTY
= DiffLines
.EMPTY
213 def __init__(self
, dash
='', empty
=''):
217 self
._dash
= dash
or compat
.uchr(0xB7)
218 self
._empty
= empty
or ' '
220 def set_digits(self
, value
):
221 self
.fmt
= '%%0%dd' % value
222 self
.empty
= self
._empty
* value
223 self
.dash
= self
._dash
* value
225 def value(self
, old
, new
):
226 old_str
= self
._format
(old
)
227 new_str
= self
._format
(new
)
228 return '%s %s' % (old_str
, new_str
)
230 def merge_value(self
, old
, base
, new
):
231 old_str
= self
._format
(old
)
232 base_str
= self
._format
(base
)
233 new_str
= self
._format
(new
)
234 return '%s %s %s' % (old_str
, base_str
, new_str
)
236 def number(self
, value
):
237 return self
.fmt
% value
239 def _format(self
, value
):
240 if value
== self
.DASH
:
242 elif value
== self
.EMPTY
:
245 result
= self
.number(value
)
249 class DiffParser(object):
250 """Parse and rewrite diffs to produce edited patches
252 This parser is used for modifying the worktree and index by constructing
253 temporary patches that are applied using "git apply".
257 def __init__(self
, filename
, diff_text
):
258 self
.filename
= filename
259 self
.hunks
= _parse_diff(diff_text
)
261 def generate_patch(self
, first_line_idx
, last_line_idx
, reverse
=False):
262 """Return a patch containing a subset of the diff"""
269 lines
= ['--- a/%s\n' % self
.filename
, '+++ b/%s\n' % self
.filename
]
273 for hunk
in self
.hunks
:
274 # skip hunks until we get to the one that contains the first
276 if hunk
.last_line_idx
< first_line_idx
:
278 # once we have processed the hunk that contains the last selected
280 if hunk
.first_line_idx
> last_line_idx
:
284 counts
= defaultdict(int)
287 for line_idx
, line
in enumerate(
288 hunk
.lines
[1:], start
=hunk
.first_line_idx
+ 1
290 line_type
, line_content
= line
[:1], line
[1:]
293 if line_type
== ADDITION
:
295 elif line_type
== DELETION
:
298 if not first_line_idx
<= line_idx
<= last_line_idx
:
299 if line_type
== ADDITION
:
300 # Skip additions that are not selected.
303 if line_type
== DELETION
:
304 # Change deletions that are not selected to context.
306 if line_type
== NO_NEWLINE
and prev_skipped
:
307 # If the line immediately before a "No newline" line was
308 # skipped (because it was an unselected addition) skip
309 # the "No newline" line as well.
311 filtered_lines
.append(line_type
+ line_content
)
312 counts
[line_type
] += 1
315 # Do not include hunks that, after filtering, have only context
316 # lines (no additions or deletions).
317 if not counts
[ADDITION
] and not counts
[DELETION
]:
320 old_count
= counts
[CONTEXT
] + counts
[DELETION
]
321 new_count
= counts
[CONTEXT
] + counts
[ADDITION
]
324 old_start
= hunk
.new_start
326 old_start
= hunk
.old_start
327 new_start
= old_start
+ start_offset
333 start_offset
+= counts
[ADDITION
] - counts
[DELETION
]
337 old_start
, old_count
, new_start
, new_count
, hunk
.heading
340 lines
.extend(filtered_lines
)
342 # If there are only two lines, that means we did not include any hunks,
346 return ''.join(lines
)
348 def generate_hunk_patch(self
, line_idx
, reverse
=False):
349 """Return a patch containing the hunk for the specified line only"""
351 for hunk
in self
.hunks
:
352 if line_idx
<= hunk
.last_line_idx
:
356 return self
.generate_patch(
357 hunk
.first_line_idx
, hunk
.last_line_idx
, reverse
=reverse