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):
13 def __init__(self
, old_start
, old_count
, new_start
, new_count
, heading
,
14 first_line_idx
, lines
):
15 self
.old_start
= old_start
16 self
.old_count
= old_count
17 self
.new_start
= new_start
18 self
.new_count
= new_count
19 self
.heading
= heading
20 self
.first_line_idx
= first_line_idx
24 def last_line_idx(self
):
25 return self
.first_line_idx
+ len(self
.lines
) - 1
28 def _parse_range_str(range_str
):
30 begin
, end
= range_str
.split(',', 1)
31 return int(begin
), int(end
)
33 return int(range_str
), 1
36 def _format_range(start
, count
):
40 return '%d,%d' % (start
, count
)
43 def _format_hunk_header(old_start
, old_count
, new_start
, new_count
,
45 return '@@ -%s +%s @@%s' % (_format_range(old_start
, old_count
),
46 _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)
58 hunks
.append(_DiffHunk(old_start
, old_count
,
60 heading
, line_idx
, lines
=[line
]))
62 # first line of the diff is not a header line
63 errmsg
= 'Malformed diff?: %s' % diff_text
64 raise AssertionError(errmsg
)
66 hunks
[-1].lines
.append(line
)
71 """Return the number of digits needed to display a number"""
73 result
= int(math
.log10(number
)) + 1
79 class DiffLines(object):
80 """Parse diffs and gather line numbers"""
91 return digits(max(self
.max_old
, self
.max_new
))
93 def parse(self
, diff_text
):
107 state
= INITIAL_STATE
109 for text
in diff_text
.splitlines():
110 if text
.startswith('@@ -'):
111 parts
= text
.split(' ', 4)
112 if parts
[0] == '@@' and parts
[3] == '@@':
114 old_start
, old_count
= _parse_range_str(parts
[1][1:])
115 new_start
, new_count
= _parse_range_str(parts
[2][1:])
118 self
.max_old
= max(old_start
+ old_count
, self
.max_old
)
119 self
.max_new
= max(new_start
+ new_count
, self
.max_new
)
120 lines
.append((self
.DASH
, self
.DASH
))
122 if state
== INITIAL_STATE
:
123 lines
.append((self
.EMPTY
, self
.EMPTY
))
124 elif text
.startswith('-'):
125 lines
.append((old_cur
, self
.EMPTY
))
127 elif text
.startswith('+'):
128 lines
.append((self
.EMPTY
, new_cur
))
130 elif text
.startswith(' '):
131 lines
.append((old_cur
, new_cur
))
144 class FormatDigits(object):
145 """Format numbers for use in diff line numbers"""
147 DASH
= DiffLines
.DASH
148 EMPTY
= DiffLines
.EMPTY
150 def __init__(self
, dash
='', empty
=''):
154 self
._dash
= dash
or compat
.unichr(0xb7)
155 self
._empty
= empty
or ' '
157 def set_digits(self
, digits
):
158 self
.fmt
= ('%%0%dd' % digits
)
159 self
.empty
= (self
._empty
* digits
)
160 self
.dash
= (self
._dash
* digits
)
162 def value(self
, old
, new
):
163 old_str
= self
._format
(old
)
164 new_str
= self
._format
(new
)
165 return ('%s %s' % (old_str
, new_str
))
167 def number(self
, value
):
168 return (self
.fmt
% value
)
170 def _format(self
, value
):
171 if value
== self
.DASH
:
173 elif value
== self
.EMPTY
:
176 result
= self
.number(value
)
180 class DiffParser(object):
181 """Parse and rewrite diffs to produce edited patches
183 This parser is used for modifying the worktree and index by constructing
184 temporary patches that are applied using "git apply".
188 def __init__(self
, filename
, diff_text
):
189 self
.filename
= filename
190 self
.hunks
= _parse_diff(diff_text
)
192 def generate_patch(self
, first_line_idx
, last_line_idx
,
194 """Return a patch containing a subset of the diff"""
201 lines
= ['--- a/%s' % self
.filename
, '+++ b/%s' % self
.filename
]
205 for hunk
in self
.hunks
:
206 # skip hunks until we get to the one that contains the first
208 if hunk
.last_line_idx
< first_line_idx
:
210 # once we have processed the hunk that contains the last selected
212 if hunk
.first_line_idx
> last_line_idx
:
216 counts
= defaultdict(int)
219 for line_idx
, line
in enumerate(hunk
.lines
[1:],
220 start
=hunk
.first_line_idx
+ 1):
221 line_type
, line_content
= line
[:1], line
[1:]
224 if line_type
== ADDITION
:
226 elif line_type
== DELETION
:
229 if not (first_line_idx
<= line_idx
<= last_line_idx
):
230 if line_type
== ADDITION
:
231 # Skip additions that are not selected.
234 elif line_type
== DELETION
:
235 # Change deletions that are not selected to context.
237 if line_type
== NO_NEWLINE
and prev_skipped
:
238 # If the line immediately before a "No newline" line was
239 # skipped (because it was an unselected addition) skip
240 # the "No newline" line as well.
242 filtered_lines
.append(line_type
+ line_content
)
243 counts
[line_type
] += 1
246 # Do not include hunks that, after filtering, have only context
247 # lines (no additions or deletions).
248 if not counts
[ADDITION
] and not counts
[DELETION
]:
251 old_count
= counts
[CONTEXT
] + counts
[DELETION
]
252 new_count
= counts
[CONTEXT
] + counts
[ADDITION
]
255 old_start
= hunk
.new_start
257 old_start
= hunk
.old_start
258 new_start
= old_start
+ start_offset
264 start_offset
+= counts
[ADDITION
] - counts
[DELETION
]
266 lines
.append(_format_hunk_header(old_start
, old_count
,
267 new_start
, new_count
,
269 lines
.extend(filtered_lines
)
271 # If there are only two lines, that means we did not include any hunks,
277 return '\n'.join(lines
)
279 def generate_hunk_patch(self
, line_idx
, reverse
=False):
280 """Return a patch containing the hunk for the specified line only"""
283 for hunk
in self
.hunks
:
284 if line_idx
<= hunk
.last_line_idx
:
286 return self
.generate_patch(hunk
.first_line_idx
, hunk
.last_line_idx
,