Refactored several methods out of the main controller.
[ugit.git] / ugitlibs / utils.py
blob2eb56fff89dffb275618089ef8095d9c89f0fd7f
1 #!/usr/bin/env python
2 import sys
3 import os
4 import re
5 import time
6 from cStringIO import StringIO
8 from PyQt4.QtCore import QProcess
10 import defaults
12 PREFIX = os.path.realpath(os.path.dirname(os.path.dirname(sys.argv[0])))
13 QMDIR = os.path.join(PREFIX, 'share', 'ugit', 'qm')
15 def get_qm_for_locale(locale):
16 regex = re.compile(r'([^\.])+\..*$')
17 match = regex.match(locale)
18 if match:
19 locale = match.group(1)
21 basename = locale.split('_')[0]
23 return os.path.join(QMDIR, basename +'.qm')
26 ICONSDIR = os.path.join(PREFIX, 'share', 'ugit', 'icons')
27 KNOWN_FILE_TYPES = {
28 'ascii c': 'c.png',
29 'python': 'script.png',
30 'ruby': 'script.png',
31 'shell': 'script.png',
32 'perl': 'script.png',
33 'java': 'script.png',
34 'assembler': 'binary.png',
35 'binary': 'binary.png',
36 'byte': 'binary.png',
37 'image': 'image.png',
40 def ident_file_type(filename):
41 '''Returns an icon based on the contents of filename.'''
42 if os.path.exists(filename):
43 fileinfo = run_cmd('file','-b',filename)
44 for filetype, iconname in KNOWN_FILE_TYPES.iteritems():
45 if filetype in fileinfo.lower():
46 return iconname
47 else:
48 return 'removed.png'
49 # Fallback for modified files of an unknown type
50 return 'generic.png'
52 def get_icon(filename):
53 '''Returns the full path to an icon file corresponding to
54 filename's contents.'''
55 icon_file = ident_file_type(filename)
56 return os.path.join(ICONSDIR, icon_file)
58 def get_staged_icon(filename):
59 '''Special-case method for staged items. These are only
60 ever 'staged' and 'removed' items in the staged list.'''
62 if os.path.exists(filename):
63 return os.path.join(ICONSDIR, 'staged.png')
64 else:
65 return os.path.join(ICONSDIR, 'removed.png')
67 def get_untracked_icon():
68 return os.path.join(ICONSDIR, 'untracked.png')
70 def get_directory_icon():
71 return os.path.join(ICONSDIR, 'dir.png')
73 def get_file_icon():
74 return os.path.join(ICONSDIR, 'generic.png')
76 def run_cmd(cmd, *args, **kwargs):
77 # Handle cmd as either a string or an argv list
78 if type(cmd) is str:
79 cmd = cmd.split(' ')
80 cmd += list(args)
81 else:
82 cmd = list(cmd + list(args))
84 child = QProcess()
85 child.setProcessChannelMode(QProcess.MergedChannels);
86 child.start(cmd[0], cmd[1:])
88 if not child.waitForStarted(): raise Exception("failed to start child")
89 if not child.waitForFinished(): raise Exception("failed to start child")
91 output = str(child.readAll())
93 # Allow run_cmd(argv, raw=True) for when we
94 # want the full, raw output(e.g. git cat-file)
95 if 'raw' in kwargs:
96 return output
97 else:
98 if 'with_status' in kwargs:
99 return child.exitCode(), output.rstrip()
100 else:
101 return output.rstrip()
103 def fork(*argv):
104 pid = os.fork()
105 if pid: return
106 os.execlp(*argv)
108 __grep_cache = {}
109 def grep(pattern, items, squash=True):
110 if pattern in __grep_cache:
111 regex = __grep_cache[pattern]
112 else:
113 regex = __grep_cache[pattern] = re.compile(pattern)
114 matched = []
115 for item in items:
116 match = regex.match(item)
117 if not match: continue
118 groups = match.groups()
119 if not groups:
120 subitems = match.group(0)
121 else:
122 if len(groups) == 1:
123 subitems = groups[0]
124 else:
125 subitems = list(groups)
126 matched.append(subitems)
128 if squash and len(matched) == 1:
129 return matched[0]
130 else:
131 return matched
133 def basename(path):
134 '''Avoid os.path.basename because we are explicitly
135 parsing git's output, which contains /'s regardless
136 of platform (a.t.m.)
138 base_regex = re.compile('(.*?/)?([^/]+)$')
139 match = base_regex.match(path)
140 if match:
141 return match.group(2)
142 else:
143 return pathstr
145 def shell_quote(*inputs):
146 '''Quote strings so that they can be suitably martialled
147 off to the shell. This method supports POSIX sh syntax.
148 This is crucial to properly handle command line arguments
149 with spaces, quotes, double-quotes, etc.'''
151 regex = re.compile('[^\w!%+,\-./:@^]')
152 quote_regex = re.compile("((?:'\\''){2,})")
154 ret = []
155 for input in inputs:
156 if not input:
157 continue
159 if '\x00' in input:
160 raise AssertionError,('No way to quote strings '
161 'containing null(\\000) bytes')
163 # = does need quoting else in command position it's a
164 # program-local environment setting
165 match = regex.search(input)
166 if match and '=' not in input:
167 # ' -> '\''
168 input = input.replace("'", "'\\''")
170 # make multiple ' in a row look simpler
171 # '\'''\'''\'' -> '"'''"'
172 quote_match = quote_regex.match(input)
173 if quote_match:
174 quotes = match.group(1)
175 input.replace(quotes,
176 ("'" *(len(quotes)/4)) + "\"'")
178 input = "'%s'" % input
179 if input.startswith("''"):
180 input = input[2:]
182 if input.endswith("''"):
183 input = input[:-2]
184 ret.append(input)
185 return ' '.join(ret)
188 def get_tmp_filename():
189 # Allow TMPDIR/TMP with a fallback to /tmp
190 return '.ugit.%s.%s' %( os.getpid(), time.time() )
192 HEADER_LENGTH = 80
193 def header(msg):
194 pad = HEADER_LENGTH - len(msg) - 4 # len(':+') + len('+:')
195 extra = pad % 2
196 pad /= 2
197 return(':+'
198 +(' ' * pad)
199 + msg
200 +(' ' *(pad + extra))
201 + '+:'
202 + '\n')
204 def parse_geom(geomstr):
205 regex = re.compile('^(\d+)x(\d+)\+(\d+),(\d+) (\d+),(\d+) (\d+),(\d+)')
206 match = regex.match(geomstr)
207 if match:
208 defaults.WIDTH = int(match.group(1))
209 defaults.HEIGHT = int(match.group(2))
210 defaults.X = int(match.group(3))
211 defaults.Y = int(match.group(4))
212 defaults.SPLITTER_TOP_0 = int(match.group(5))
213 defaults.SPLITTER_TOP_1 = int(match.group(6))
214 defaults.SPLITTER_BOTTOM_0 = int(match.group(7))
215 defaults.SPLITTER_BOTTOM_1 = int(match.group(8))
217 return (defaults.WIDTH, defaults.HEIGHT,
218 defaults.X, defaults.Y,
219 defaults.SPLITTER_TOP_0, defaults.SPLITTER_TOP_1,
220 defaults.SPLITTER_BOTTOM_0, defaults.SPLITTER_BOTTOM_1)
222 def get_geom():
223 return '%dx%d+%d,%d %d,%d %d,%d' % (
224 defaults.WIDTH, defaults.HEIGHT,
225 defaults.X, defaults.Y,
226 defaults.SPLITTER_TOP_0, defaults.SPLITTER_TOP_1,
227 defaults.SPLITTER_BOTTOM_0, defaults.SPLITTER_BOTTOM_1)
229 def project_name():
230 return os.path.basename(defaults.DIRECTORY)
232 def slurp(path):
233 file = open(path)
234 slushy = file.read()
235 file.close()
236 return slushy
238 def write(path, contents):
239 file = open(path, 'w')
240 file.write(contents)
241 file.close()
244 class DiffParser(object):
245 def __init__(self, model,
246 filename='', cached=True):
247 self.__header_pattern = re.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
248 self.__headers = []
250 self.__idx = -1
251 self.__diffs = []
252 self.__diff_spans = []
253 self.__diff_offsets = []
255 self.start = None
256 self.end = None
257 self.offset = None
258 self.diffs = []
259 self.selected = []
261 (header, diff) = \
262 model.diff(filename=filename, with_diff_header=True,
263 cached=cached, reverse=cached)
265 self.model = model
266 self.diff = diff
267 self.header = header
268 self.parse_diff(diff)
270 # Always index into the non-reversed diff
271 self.fwd_header, self.fwd_diff = \
272 model.diff(filename=filename, with_diff_header=True,
273 cached=cached, reverse=False)
275 def write_diff(self,filename,which,selected=False,noop=False):
276 if not noop and which < len(self.diffs):
277 diff = self.diffs[which]
278 write(filename, self.header + os.linesep + diff + os.linesep)
279 return True
280 else:
281 return False
283 def get_diffs(self):
284 return self.__diffs
286 def get_diff_subset(self, diff, start, end):
287 newdiff = []
288 diffguts = os.linesep.join(self.__diffs[diff])
290 offset = self.__diff_spans[diff][0]
292 local_offset = 0
294 adds = 0
295 deletes = 0
297 for line in self.__diffs[diff]:
299 line_start = offset + local_offset
300 local_offset += len(line) + 1
301 line_end = offset + local_offset
303 # |line1 |line2 |line3|
304 # |selection----|
305 # '-start '-end
307 # selection has head of diff (line3)
308 if start < line_start and end > line_start and end < line_end:
309 newdiff.append(line)
310 if line.startswith('+'):
311 adds += 1
312 if line.startswith('-'):
313 deletes += 1
314 # selection has all of diff (line2)
315 elif start <= line_start and end >= line_end:
316 newdiff.append(line)
317 if line.startswith('+'):
318 adds += 1
319 if line.startswith('-'):
320 deletes += 1
321 # selection has tail of diff (line1)
322 elif start >= line_start and start < line_end - 1:
323 newdiff.append(line)
324 if line.startswith('+'):
325 adds += 1
326 if line.startswith('-'):
327 deletes += 1
328 else:
329 # Don't add new lines unless selected
330 if line.startswith('+'):
331 continue
332 elif line.startswith('-'):
333 # Don't remove lines unless selected
334 newdiff.append(' ' + line[1:])
335 else:
336 newdiff.append(line)
338 new_count = self.__headers[diff][1] + adds - deletes
340 if new_count != self.__headers[diff][3]:
341 header = '@@ -%d,%d +%d,%d @@' % (
342 self.__headers[diff][0],
343 self.__headers[diff][1],
344 self.__headers[diff][2],
345 new_count)
346 newdiff[0] = header
348 return (self.header
349 + os.linesep
350 + os.linesep.join(newdiff)
351 + os.linesep)
353 def get_spans(self):
354 return self.__diff_spans
356 def get_offsets(self):
357 return self.__diff_offsets
359 def set_diff_to_offset(self, offset):
360 self.offset = offset
361 self.diffs, self.selected = self.get_diff_for_offset(offset)
363 def set_diffs_to_range(self, start, end):
364 self.start = start
365 self.end = end
366 self.diffs, self.selected = self.get_diffs_for_range(start,end)
368 def get_diff_for_offset(self, offset):
369 for idx, diff_offset in enumerate(self.__diff_offsets):
370 if offset < diff_offset:
371 return ([os.linesep.join(self.__diffs[idx])],
372 [idx])
373 return ([],[])
375 def get_diffs_for_range(self, start, end):
376 diffs = []
377 indices = []
378 for idx, span in enumerate(self.__diff_spans):
379 has_end_of_diff = start >= span[0] and start < span[1]
380 has_all_of_diff = start <= span[0] and end >= span[1]
381 has_head_of_diff = end >= span[0] and end <= span[1]
383 selected_diff =(has_end_of_diff
384 or has_all_of_diff
385 or has_head_of_diff)
387 if selected_diff:
388 diff = os.linesep.join(self.__diffs[idx])
389 diffs.append(diff)
390 indices.append(idx)
391 return diffs, indices
393 def parse_diff(self, diff):
394 total_offset = 0
395 self.__idx = -1
396 self.__headers = []
398 for idx, line in enumerate(diff.splitlines()):
400 match = self.__header_pattern.match(line)
401 if match:
402 self.__headers.append([
403 int(match.group(1)),
404 int(match.group(2)),
405 int(match.group(3)),
406 int(match.group(4))
409 self.__diffs.append( [line] )
411 line_len = len(line) + 1
412 self.__diff_spans.append([total_offset,
413 total_offset + line_len])
415 total_offset += line_len
416 self.__diff_offsets.append(total_offset)
418 self.__idx += 1
419 else:
420 if self.__idx < 0:
421 errmsg = 'Malformed diff?\n\n%s' % diff
422 raise AssertionError, errmsg
424 line_len = len(line) + 1
425 total_offset += line_len
427 self.__diffs[self.__idx].append(line)
428 self.__diff_spans[-1][-1] += line_len
429 self.__diff_offsets[self.__idx] += line_len
431 def process_diff_selection(self, selected, offset, selection):
432 if selection:
433 start = self.fwd_diff.index(selection)
434 end = start + len(selection)
435 self.set_diffs_to_range(start, end)
436 else:
437 self.set_diff_to_offset(offset)
438 selected = False
440 # Process diff selection only
441 if selected:
442 for idx in self.selected:
443 contents = self.get_diff_subset(idx, start, end)
444 if contents:
445 tmpfile = get_tmp_filename()
446 write(tmpfile, contents)
447 self.model.apply_diff(tmpfile)
448 os.unlink(tmpfile)
449 # Process a complete hunk
450 else:
451 for idx, diff in enumerate(self.diffs):
452 tmpfile = get_tmp_filename()
453 if self.write_diff(tmpfile,idx):
454 self.model.apply_diff(tmpfile)
455 os.unlink(tmpfile)