hotkeys: Change "Tree Navigation" to "Navigation"
[git-cola.git] / cola / diffparse.py
blobaa32bc444af9d19f36a859ca53c6ccbc875ad122
1 import os
2 import re
4 from cola import utils
5 from cola import gitcmds
8 class DiffParser(object):
9 """Handles parsing diff for use by the interactive index editor."""
10 def __init__(self, model, filename='',
11 cached=True, reverse=False):
13 self._header_re = re.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
14 self._headers = []
16 self._idx = -1
17 self._diffs = []
18 self._diff_spans = []
19 self._diff_offsets = []
21 self.head = model.head
22 self.amending = model.amending()
23 self.start = None
24 self.end = None
25 self.offset = None
26 self.diffs = []
27 self.selected = []
29 (header, diff) = gitcmds.diff_helper(head=self.head,
30 amending=self.amending,
31 filename=filename,
32 with_diff_header=True,
33 cached=cached,
34 reverse=cached or reverse)
35 self.model = model
36 self.diff = diff
37 self.header = header
38 self.parse_diff(diff)
40 # Always index into the non-reversed diff
41 self.fwd_header, self.fwd_diff = \
42 gitcmds.diff_helper(head=self.head,
43 amending=self.amending,
44 filename=filename,
45 with_diff_header=True,
46 cached=cached)
48 def write_diff(self,filename,which,selected=False,noop=False):
49 """Writes a new diff corresponding to the user's selection."""
50 if not noop and which < len(self.diffs):
51 diff = self.diffs[which]
52 utils.write(filename, self.header + '\n' + diff + '\n')
53 return True
54 else:
55 return False
57 def diffs(self):
58 """Returns the list of diffs."""
59 return self._diffs
61 def diff_subset(self, diff, start, end):
62 """Processes the diffs and returns a selected subset from that diff.
63 """
64 adds = 0
65 deletes = 0
66 newdiff = []
67 local_offset = 0
68 offset = self._diff_spans[diff][0]
69 diffguts = '\n'.join(self._diffs[diff])
71 for line in self._diffs[diff]:
72 line_start = offset + local_offset
73 local_offset += len(line) + 1 #\n
74 line_end = offset + local_offset
75 # |line1 |line2 |line3 |
76 # |--selection--|
77 # '-start '-end
78 # selection has head of diff (line3)
79 if start < line_start and end > line_start and end < line_end:
80 newdiff.append(line)
81 if line.startswith('+'):
82 adds += 1
83 if line.startswith('-'):
84 deletes += 1
85 # selection has all of diff (line2)
86 elif start <= line_start and end >= line_end:
87 newdiff.append(line)
88 if line.startswith('+'):
89 adds += 1
90 if line.startswith('-'):
91 deletes += 1
92 # selection has tail of diff (line1)
93 elif start >= line_start and start < line_end - 1:
94 newdiff.append(line)
95 if line.startswith('+'):
96 adds += 1
97 if line.startswith('-'):
98 deletes += 1
99 else:
100 # Don't add new lines unless selected
101 if line.startswith('+'):
102 continue
103 elif line.startswith('-'):
104 # Don't remove lines unless selected
105 newdiff.append(' ' + line[1:])
106 else:
107 newdiff.append(line)
109 new_count = self._headers[diff][1] + adds - deletes
110 if new_count != self._headers[diff][3]:
111 header = '@@ -%d,%d +%d,%d @@' % (
112 self._headers[diff][0],
113 self._headers[diff][1],
114 self._headers[diff][2],
115 new_count)
116 newdiff[0] = header
118 return (self.header + '\n' + '\n'.join(newdiff) + '\n')
120 def spans(self):
121 """Returns the line spans of each hunk."""
122 return self._diff_spans
124 def offsets(self):
125 """Returns the offsets."""
126 return self._diff_offsets
128 def set_diff_to_offset(self, offset):
129 """Sets the diff selection to be the hunk at a particular offset."""
130 self.offset = offset
131 self.diffs, self.selected = self.diff_for_offset(offset)
133 def set_diffs_to_range(self, start, end):
134 """Sets the diff selection to be a range of hunks."""
135 self.start = start
136 self.end = end
137 self.diffs, self.selected = self.diffs_for_range(start,end)
139 def diff_for_offset(self, offset):
140 """Returns the hunks for a particular offset."""
141 for idx, diff_offset in enumerate(self._diff_offsets):
142 if offset < diff_offset:
143 return (['\n'.join(self._diffs[idx])], [idx])
144 return ([],[])
146 def diffs_for_range(self, start, end):
147 """Returns the hunks for a selected range."""
148 diffs = []
149 indices = []
150 for idx, span in enumerate(self._diff_spans):
151 has_end_of_diff = start >= span[0] and start < span[1]
152 has_all_of_diff = start <= span[0] and end >= span[1]
153 has_head_of_diff = end >= span[0] and end <= span[1]
155 selected_diff =(has_end_of_diff
156 or has_all_of_diff
157 or has_head_of_diff)
158 if selected_diff:
159 diff = '\n'.join(self._diffs[idx])
160 diffs.append(diff)
161 indices.append(idx)
162 return diffs, indices
164 def parse_diff(self, diff):
165 """Parses a diff and extracts headers, offsets, hunks, etc.
167 total_offset = 0
168 self._idx = -1
169 self._headers = []
171 for idx, line in enumerate(diff.split('\n')):
172 match = self._header_re.match(line)
173 if match:
174 self._headers.append([
175 int(match.group(1)),
176 int(match.group(2)),
177 int(match.group(3)),
178 int(match.group(4))
180 self._diffs.append( [line] )
182 line_len = len(line) + 1 #\n
183 self._diff_spans.append([total_offset,
184 total_offset + line_len])
185 total_offset += line_len
186 self._diff_offsets.append(total_offset)
187 self._idx += 1
188 else:
189 if self._idx < 0:
190 errmsg = 'Malformed diff?\n\n%s' % diff
191 raise AssertionError, errmsg
192 line_len = len(line) + 1
193 total_offset += line_len
195 self._diffs[self._idx].append(line)
196 self._diff_spans[-1][-1] += line_len
197 self._diff_offsets[self._idx] += line_len
199 def process_diff_selection(self, selected, offset, selection,
200 apply_to_worktree=False):
201 """Processes a diff selection and applies changes to git."""
202 if selection:
203 # qt destroys \r\n and makes it \n with no way of going back.
204 # boo! we work around that here.
205 # I think this was win32-specific. We might want to do
206 # this on win32 only (TODO verify)
207 if selection not in self.fwd_diff:
208 special_selection = selection.replace('\n', '\r\n')
209 if special_selection in self.fwd_diff:
210 selection = special_selection
211 else:
212 return 0, ''
213 start = self.fwd_diff.index(selection)
214 end = start + len(selection)
215 self.set_diffs_to_range(start, end)
216 else:
217 self.set_diff_to_offset(offset)
218 selected = False
220 output = ''
221 status = 0
222 # Process diff selection only
223 if selected:
224 for idx in self.selected:
225 contents = self.diff_subset(idx, start, end)
226 if not contents:
227 continue
228 tmpfile = utils.tmp_filename('selection')
229 utils.write(tmpfile, contents)
230 if apply_to_worktree:
231 stat, out = self.model.apply_diff_to_worktree(tmpfile)
232 output += out
233 status = max(status, stat)
234 else:
235 stat, out = self.model.apply_diff(tmpfile)
236 output += out
237 status = max(status, stat)
238 os.unlink(tmpfile)
239 # Process a complete hunk
240 else:
241 for idx, diff in enumerate(self.diffs):
242 tmpfile = utils.tmp_filename('patch%02d' % idx)
243 if not self.write_diff(tmpfile,idx):
244 continue
245 if apply_to_worktree:
246 stat, out = self.model.apply_diff_to_worktree(tmpfile)
247 output += out
248 status = max(status, stat)
249 else:
250 stat, out = self.model.apply_diff(tmpfile)
251 output += out
252 status = max(status, stat)
253 os.unlink(tmpfile)
254 return status, output