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