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