cola: move the cola.main package to cola.widgets.main and cola.models.main
[git-cola.git] / cola / diffparse.py
blob9b5a07bbba25c713a77deec03a6f609c6193629a
1 import os
2 import re
4 from cola import core
5 from cola import gitcmds
6 from cola import gitcfg
7 from cola import utils
10 class Range(object):
12 def __init__(self, begin, end):
13 self.begin = self._parse(begin)
14 self.end = self._parse(end)
16 def _parse(self, range_str):
17 if ',' in range_str:
18 begin, end = range_str.split(',')
19 return [int(begin), int(end)]
20 else:
21 return [int(range_str), int(range_str)]
23 def make(self):
24 return '@@ -%s +%s @@' % (self._span(self.begin), self._span(self.end))
26 def set_begin_count(self, count):
27 self._set_count(self.begin, count)
29 def set_end_count(self, count):
30 self._set_count(self.end, count)
32 def _set_count(self, which, count):
33 if count != which[1]:
34 which[1] = count
35 if count == 1 and which[0] == 0:
36 # the file would be empty in the diff, but we're only
37 # partially applying it, and thus it's not a +0,0 diff
38 # anymore.
39 which[0] = 1
41 def _span(self, seq):
42 a = seq[0]
43 b = seq[1]
44 if a == b and a == 1:
45 return '%d' % a
46 else:
47 return '%d,%d' % (a, b)
50 class DiffSource(object):
51 def get(self, head, amending, filename, cached, reverse):
52 return gitcmds.diff_helper(head=head,
53 amending=amending,
54 filename=filename,
55 with_diff_header=True,
56 cached=cached,
57 reverse=reverse)
60 class DiffParser(object):
62 """Handles parsing diff for use by the interactive index editor."""
64 HEADER_RE = re.compile(r'^@@ -([0-9,]+) \+([0-9,]+) @@.*')
66 def __init__(self, model, filename='',
67 cached=True, reverse=False,
68 diff_source=None):
70 self._idx = -1
71 self._diffs = []
72 self._diff_spans = []
73 self._diff_offsets = []
74 self._ranges = []
76 self.config = gitcfg.instance()
77 self.head = model.head
78 self.amending = model.amending()
79 self.start = None
80 self.end = None
81 self.offset = None
82 self.diff_sel = []
83 self.selected = []
84 self.filename = filename
85 self.diff_source = diff_source or DiffSource()
87 (header, diff) = self.diff_source.get(self.head, self.amending,
88 filename, cached,
89 cached or reverse)
90 self.model = model
91 self.diff = diff
92 self.header = header
93 self.parse_diff(diff)
95 # Always index into the non-reversed diff
96 self.fwd_header, self.fwd_diff = \
97 self.diff_source.get(self.head,
98 self.amending,
99 filename,
100 cached, False)
102 def write_diff(self,filename,which,selected=False,noop=False):
103 """Writes a new diff corresponding to the user's selection."""
104 if not noop and which < len(self.diff_sel):
105 diff = self.diff_sel[which]
106 encoding = self.config.file_encoding(self.filename)
107 core.write(filename, self.header + '\n' + diff + '\n',
108 encoding=encoding)
109 return True
110 else:
111 return False
113 def ranges(self):
114 """Return the diff header ranges"""
115 return self._ranges
117 def diffs(self):
118 """Returns the list of diffs."""
119 return self._diffs
121 def diff_subset(self, diff, start, end):
122 """Processes the diffs and returns a selected subset from that diff.
124 adds = 0
125 deletes = 0
126 existing = 0
127 newdiff = []
128 local_offset = 0
129 offset = self._diff_spans[diff][0]
131 ADD = '+'
132 DEL = '-'
133 NOP = ' '
135 for line in self._diffs[diff]:
136 line_start = offset + local_offset
137 local_offset += len(line) + 1 #\n
138 line_end = offset + local_offset
139 # |line1 |line2 |line3 |
140 # |--selection--|
141 # '-start '-end
143 # selection has head of diff (line3)
144 has_head = start <= line_start and end > line_start and end <= line_end
145 # selection has all of diff (line2)
146 has_all = start <= line_start and end >= line_end
147 # selection has tail of diff (line1)
148 has_tail = start >= line_start and start < line_end - 1
150 action = line[0:1]
151 if has_head or has_all or has_tail:
152 newdiff.append(line)
153 if action == ADD:
154 adds += 1
155 elif action == DEL:
156 deletes += 1
157 elif action == NOP:
158 existing += 1
159 else:
160 # Don't add new lines unless selected
161 if action == ADD:
162 continue
163 elif action == DEL:
164 # Don't remove lines unless selected
165 newdiff.append(' ' + line[1:])
166 existing += 1
167 elif action == NOP:
168 newdiff.append(line)
169 existing += 1
170 else:
171 newdiff.append(line)
173 diff_range = self._ranges[diff]
174 begin_count = existing + deletes
175 end_count = existing + adds
177 diff_range.set_begin_count(begin_count)
178 diff_range.set_end_count(end_count)
179 newdiff[0] = diff_range.make()
181 return (self.header + '\n' + '\n'.join(newdiff) + '\n')
183 def spans(self):
184 """Returns the line spans of each hunk."""
185 return self._diff_spans
187 def offsets(self):
188 """Returns the offsets."""
189 return self._diff_offsets
191 def set_diff_to_offset(self, offset):
192 """Sets the diff selection to be the hunk at a particular offset."""
193 self.offset = offset
194 self.diff_sel, self.selected = self.diff_for_offset(offset)
196 def set_diffs_to_range(self, start, end):
197 """Sets the diff selection to be a range of hunks."""
198 self.start = start
199 self.end = end
200 self.diff_sel, self.selected = self.diffs_for_range(start,end)
202 def diff_for_offset(self, offset):
203 """Returns the hunks for a particular offset."""
204 for idx, diff_offset in enumerate(self._diff_offsets):
205 if offset < diff_offset:
206 return (['\n'.join(self._diffs[idx])], [idx])
207 return ([],[])
209 def diffs_for_range(self, start, end):
210 """Returns the hunks for a selected range."""
211 diffs = []
212 indices = []
213 for idx, span in enumerate(self._diff_spans):
214 has_end_of_diff = start >= span[0] and start < span[1]
215 has_all_of_diff = start <= span[0] and end >= span[1]
216 has_head_of_diff = end >= span[0] and end <= span[1]
218 selected_diff =(has_end_of_diff
219 or has_all_of_diff
220 or has_head_of_diff)
221 if selected_diff:
222 diff = '\n'.join(self._diffs[idx])
223 diffs.append(diff)
224 indices.append(idx)
225 return diffs, indices
227 def parse_diff(self, diff):
228 """Parses a diff and extracts headers, offsets, hunks, etc.
230 total_offset = 0
231 self._idx = -1
233 for line in diff.split('\n'):
234 match = self.HEADER_RE.match(line)
235 if match:
236 self._ranges.append(Range(match.group(1), match.group(2)))
237 self._diffs.append([line])
239 line_len = len(line) + 1 #\n
240 self._diff_spans.append([total_offset,
241 total_offset + line_len])
242 total_offset += line_len
243 self._diff_offsets.append(total_offset)
244 self._idx += 1
245 continue
247 if self._idx < 0:
248 errmsg = 'Malformed diff?: %s' % diff
249 raise AssertionError(errmsg)
251 line_len = len(line) + 1
252 total_offset += line_len
254 self._diffs[self._idx].append(line)
255 self._diff_spans[-1][-1] += line_len
256 self._diff_offsets[self._idx] += line_len
258 def process_diff_selection(self, selected, offset, selection,
259 apply_to_worktree=False):
260 """Processes a diff selection and applies changes to git."""
261 if selection:
262 # qt destroys \r\n and makes it \n with no way of going back.
263 # boo! we work around that here.
264 # I think this was win32-specific. We might want to do
265 # this on win32 only (TODO verify)
266 if selection not in self.fwd_diff:
267 special_selection = selection.replace('\n', '\r\n')
268 if special_selection in self.fwd_diff:
269 selection = special_selection
270 else:
271 return 0, ''
272 start = self.fwd_diff.index(selection)
273 end = start + len(selection)
274 self.set_diffs_to_range(start, end)
275 else:
276 self.set_diff_to_offset(offset)
277 selected = False
279 output = ''
280 error = ''
281 status = 0
282 # Process diff selection only
283 if selected:
284 encoding = self.config.file_encoding(self.filename)
285 for idx in self.selected:
286 contents = self.diff_subset(idx, start, end)
287 if not contents:
288 continue
289 tmpfile = utils.tmp_filename('selection')
290 core.write(tmpfile, contents, encoding=encoding)
291 if apply_to_worktree:
292 stat, out, err = self.model.apply_diff_to_worktree(tmpfile)
293 output += out
294 error += err
295 status = max(status, stat)
296 else:
297 stat, out, err = self.model.apply_diff(tmpfile)
298 output += out
299 error += err
300 status = max(status, stat)
301 os.unlink(tmpfile)
302 # Process a complete hunk
303 else:
304 for idx, diff in enumerate(self.diff_sel):
305 tmpfile = utils.tmp_filename('patch%02d' % idx)
306 if not self.write_diff(tmpfile,idx):
307 continue
308 if apply_to_worktree:
309 stat, out, err = self.model.apply_diff_to_worktree(tmpfile)
310 output += out
311 error += err
312 status = max(status, stat)
313 else:
314 stat, out, err = self.model.apply_diff(tmpfile)
315 output += out
316 error += err
317 status = max(status, stat)
318 os.unlink(tmpfile)
319 return status, output, error