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 def __init__(self
, old_start
, old_count
, new_start
, new_count
, heading
,
15 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
)
34 return int(range_str
), 1
37 def _format_range(start
, count
):
41 return '%d,%d' % (start
, count
)
44 def _format_hunk_header(old_start
, old_count
, new_start
, new_count
,
46 return '@@ -%s +%s @@%s' % (_format_range(old_start
, old_count
),
47 _format_range(new_start
, new_count
),
51 def _parse_diff(diff_text
):
53 for line_idx
, line
in enumerate(diff_text
.split('\n')):
54 match
= _HUNK_HEADER_RE
.match(line
)
56 old_start
, old_count
= _parse_range_str(match
.group(1))
57 new_start
, new_count
= _parse_range_str(match
.group(2))
58 heading
= match
.group(3)
59 hunks
.append(_DiffHunk(old_start
, old_count
,
61 heading
, line_idx
, lines
=[line
]))
63 # first line of the diff is not a header line
64 errmsg
= 'Malformed diff?: %s' % diff_text
65 raise AssertionError(errmsg
)
67 hunks
[-1].lines
.append(line
)
72 """Return the number of digits needed to display a number"""
74 result
= int(math
.log10(number
)) + 1
80 class Counter(object):
81 """Keep track of a diff range's values"""
83 def __init__(self
, value
=0, max_value
=-1):
85 self
.max_value
= max_value
86 self
._initial
_max
_value
= max_value
89 """Reset the max counter and return self for convenience"""
90 self
.max_value
= self
._initial
_max
_value
93 def parse(self
, range_str
):
94 """Parse a diff range and setup internal state"""
95 start
, count
= _parse_range_str(range_str
)
97 self
.max_value
= max(start
+ count
, self
.max_value
)
99 def tick(self
, amount
=1):
100 """Return the current value and increment to the next"""
106 class DiffLines(object):
107 """Parse diffs and gather line numbers"""
117 # merge <ours> <theirs> <new>
120 self
.ours
= Counter()
121 self
.theirs
= Counter()
124 return digits(max(self
.old
.max_value
, self
.new
.max_value
,
125 self
.ours
.max_value
, self
.theirs
.max_value
))
127 def parse(self
, diff_text
):
131 state
= INITIAL_STATE
132 merge
= self
.merge
= False
133 NO_NEWLINE
= '\\ No newline at end of file'
135 old
= self
.old
.reset()
136 new
= self
.new
.reset()
137 ours
= self
.ours
.reset()
138 theirs
= self
.theirs
.reset()
140 for text
in diff_text
.splitlines():
141 if text
.startswith('@@ -'):
142 parts
= text
.split(' ', 4)
143 if parts
[0] == '@@' and parts
[3] == '@@':
145 old
.parse(parts
[1][1:])
146 new
.parse(parts
[2][1:])
147 lines
.append((self
.DASH
, self
.DASH
))
149 if text
.startswith('@@@ -'):
150 self
.merge
= merge
= True
151 parts
= text
.split(' ', 5)
152 if parts
[0] == '@@@' and parts
[4] == '@@@':
154 ours
.parse(parts
[1][1:])
155 theirs
.parse(parts
[2][1:])
156 new
.parse(parts
[3][1:])
157 lines
.append((self
.DASH
, self
.DASH
, self
.DASH
))
159 if state
== INITIAL_STATE
or text
== NO_NEWLINE
:
161 lines
.append((self
.EMPTY
, self
.EMPTY
, self
.EMPTY
))
163 lines
.append((self
.EMPTY
, self
.EMPTY
))
164 elif not merge
and text
.startswith('-'):
165 lines
.append((old
.tick(), self
.EMPTY
))
166 elif merge
and text
.startswith('- '):
167 lines
.append((self
.EMPTY
, theirs
.tick(), self
.EMPTY
))
168 elif merge
and text
.startswith(' -'):
169 lines
.append((self
.EMPTY
, theirs
.tick(), self
.EMPTY
))
170 elif merge
and text
.startswith('--'):
171 lines
.append((ours
.tick(), theirs
.tick(), self
.EMPTY
))
172 elif not merge
and text
.startswith('+'):
173 lines
.append((self
.EMPTY
, new
.tick()))
174 elif merge
and text
.startswith('++'):
175 lines
.append((self
.EMPTY
, self
.EMPTY
, new
.tick()))
176 elif merge
and text
.startswith('+ '):
177 lines
.append((self
.EMPTY
, theirs
.tick(), new
.tick()))
178 elif merge
and text
.startswith(' +'):
179 lines
.append((ours
.tick(), self
.EMPTY
, new
.tick()))
180 elif not merge
and text
.startswith(' '):
181 lines
.append((old
.tick(), new
.tick()))
182 elif merge
and text
.startswith(' '):
183 lines
.append((ours
.tick(), theirs
.tick(), new
.tick()))
190 state
= INITIAL_STATE
192 lines
.append((self
.EMPTY
, self
.EMPTY
, self
.EMPTY
))
194 lines
.append((self
.EMPTY
, self
.EMPTY
))
199 class FormatDigits(object):
200 """Format numbers for use in diff line numbers"""
202 DASH
= DiffLines
.DASH
203 EMPTY
= DiffLines
.EMPTY
205 def __init__(self
, dash
='', empty
=''):
209 self
._dash
= dash
or compat
.unichr(0xb7)
210 self
._empty
= empty
or ' '
212 def set_digits(self
, digits
):
213 self
.fmt
= ('%%0%dd' % digits
)
214 self
.empty
= (self
._empty
* digits
)
215 self
.dash
= (self
._dash
* digits
)
217 def value(self
, old
, new
):
218 old_str
= self
._format
(old
)
219 new_str
= self
._format
(new
)
220 return ('%s %s' % (old_str
, new_str
))
222 def merge_value(self
, old
, base
, new
):
223 old_str
= self
._format
(old
)
224 base_str
= self
._format
(base
)
225 new_str
= self
._format
(new
)
226 return ('%s %s %s' % (old_str
, base_str
, new_str
))
228 def number(self
, value
):
229 return (self
.fmt
% value
)
231 def _format(self
, value
):
232 if value
== self
.DASH
:
234 elif value
== self
.EMPTY
:
237 result
= self
.number(value
)
241 class DiffParser(object):
242 """Parse and rewrite diffs to produce edited patches
244 This parser is used for modifying the worktree and index by constructing
245 temporary patches that are applied using "git apply".
249 def __init__(self
, filename
, diff_text
):
250 self
.filename
= filename
251 self
.hunks
= _parse_diff(diff_text
)
253 def generate_patch(self
, first_line_idx
, last_line_idx
,
255 """Return a patch containing a subset of the diff"""
262 lines
= ['--- a/%s' % self
.filename
, '+++ b/%s' % self
.filename
]
266 for hunk
in self
.hunks
:
267 # skip hunks until we get to the one that contains the first
269 if hunk
.last_line_idx
< first_line_idx
:
271 # once we have processed the hunk that contains the last selected
273 if hunk
.first_line_idx
> last_line_idx
:
277 counts
= defaultdict(int)
280 for line_idx
, line
in enumerate(hunk
.lines
[1:],
281 start
=hunk
.first_line_idx
+ 1):
282 line_type
, line_content
= line
[:1], line
[1:]
285 if line_type
== ADDITION
:
287 elif line_type
== DELETION
:
290 if not (first_line_idx
<= line_idx
<= last_line_idx
):
291 if line_type
== ADDITION
:
292 # Skip additions that are not selected.
295 elif line_type
== DELETION
:
296 # Change deletions that are not selected to context.
298 if line_type
== NO_NEWLINE
and prev_skipped
:
299 # If the line immediately before a "No newline" line was
300 # skipped (because it was an unselected addition) skip
301 # the "No newline" line as well.
303 filtered_lines
.append(line_type
+ line_content
)
304 counts
[line_type
] += 1
307 # Do not include hunks that, after filtering, have only context
308 # lines (no additions or deletions).
309 if not counts
[ADDITION
] and not counts
[DELETION
]:
312 old_count
= counts
[CONTEXT
] + counts
[DELETION
]
313 new_count
= counts
[CONTEXT
] + counts
[ADDITION
]
316 old_start
= hunk
.new_start
318 old_start
= hunk
.old_start
319 new_start
= old_start
+ start_offset
325 start_offset
+= counts
[ADDITION
] - counts
[DELETION
]
327 lines
.append(_format_hunk_header(old_start
, old_count
,
328 new_start
, new_count
,
330 lines
.extend(filtered_lines
)
332 # If there are only two lines, that means we did not include any hunks,
338 return '\n'.join(lines
)
340 def generate_hunk_patch(self
, line_idx
, reverse
=False):
341 """Return a patch containing the hunk for the specified line only"""
344 for hunk
in self
.hunks
:
345 if line_idx
<= hunk
.last_line_idx
:
347 return self
.generate_patch(hunk
.first_line_idx
, hunk
.last_line_idx
,