Added option edit/save support via git config.
[ugit.git] / ugitlibs / utils.py
blob85ab42dd18c79589ef06dd6b0eec3b362a41b3b0
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()
86 if 'stderr' not in kwargs:
87 child.setProcessChannelMode(QProcess.MergedChannels);
89 child.start(cmd[0], cmd[1:])
91 if not child.waitForStarted(): return ''
92 if not child.waitForFinished(): return ''
94 output = str(child.readAll())
96 # run_cmd(argv, raw=True) if we want the full, raw output
97 if 'raw' in kwargs:
98 return output
99 else:
100 if 'with_status' in kwargs:
101 return child.exitCode(), output.rstrip()
102 else:
103 return output.rstrip()
105 def fork(*argv):
106 pid = os.fork()
107 if pid: return
108 os.execlp(*argv)
110 # c = a - b
111 def sublist(a,b):
112 c = []
113 for item in a:
114 if item not in b:
115 c.append(item)
116 return c
118 __grep_cache = {}
119 def grep(pattern, items, squash=True):
120 isdict = type(items) is dict
121 if pattern in __grep_cache:
122 regex = __grep_cache[pattern]
123 else:
124 regex = __grep_cache[pattern] = re.compile(pattern)
125 matched = []
126 matchdict = {}
127 for item in items:
128 match = regex.match(item)
129 if not match: continue
130 groups = match.groups()
131 if not groups:
132 subitems = match.group(0)
133 else:
134 if len(groups) == 1:
135 subitems = groups[0]
136 else:
137 subitems = list(groups)
138 if isdict:
139 matchdict[item] = items[item]
140 else:
141 matched.append(subitems)
143 if isdict:
144 return matchdict
145 else:
146 if squash and len(matched) == 1:
147 return matched[0]
148 else:
149 return matched
151 def basename(path):
152 '''Avoid os.path.basename because we are explicitly
153 parsing git's output, which contains /'s regardless
154 of platform (a.t.m.)
156 base_regex = re.compile('(.*?/)?([^/]+)$')
157 match = base_regex.match(path)
158 if match:
159 return match.group(2)
160 else:
161 return pathstr
163 def shell_quote(*inputs):
164 '''Quote strings so that they can be suitably martialled
165 off to the shell. This method supports POSIX sh syntax.
166 This is crucial to properly handle command line arguments
167 with spaces, quotes, double-quotes, etc.'''
169 regex = re.compile('[^\w!%+,\-./:@^]')
170 quote_regex = re.compile("((?:'\\''){2,})")
172 ret = []
173 for input in inputs:
174 if not input:
175 continue
177 if '\x00' in input:
178 raise AssertionError,('No way to quote strings '
179 'containing null(\\000) bytes')
181 # = does need quoting else in command position it's a
182 # program-local environment setting
183 match = regex.search(input)
184 if match and '=' not in input:
185 # ' -> '\''
186 input = input.replace("'", "'\\''")
188 # make multiple ' in a row look simpler
189 # '\'''\'''\'' -> '"'''"'
190 quote_match = quote_regex.match(input)
191 if quote_match:
192 quotes = match.group(1)
193 input.replace(quotes,
194 ("'" *(len(quotes)/4)) + "\"'")
196 input = "'%s'" % input
197 if input.startswith("''"):
198 input = input[2:]
200 if input.endswith("''"):
201 input = input[:-2]
202 ret.append(input)
203 return ' '.join(ret)
206 def get_tmp_filename():
207 # Allow TMPDIR/TMP with a fallback to /tmp
208 return '.ugit.%s.%s' %( os.getpid(), time.time() )
210 HEADER_LENGTH = 80
211 def header(msg):
212 pad = HEADER_LENGTH - len(msg) - 4 # len(':+') + len('+:')
213 extra = pad % 2
214 pad /= 2
215 return(':+'
216 +(' ' * pad)
217 + msg
218 +(' ' *(pad + extra))
219 + '+:'
220 + '\n')
222 def parse_geom(geomstr):
223 regex = re.compile('^(\d+)x(\d+)\+(\d+),(\d+) (\d+),(\d+) (\d+),(\d+)')
224 match = regex.match(geomstr)
225 if match:
226 defaults.WIDTH = int(match.group(1))
227 defaults.HEIGHT = int(match.group(2))
228 defaults.X = int(match.group(3))
229 defaults.Y = int(match.group(4))
230 defaults.SPLITTER_TOP_0 = int(match.group(5))
231 defaults.SPLITTER_TOP_1 = int(match.group(6))
232 defaults.SPLITTER_BOTTOM_0 = int(match.group(7))
233 defaults.SPLITTER_BOTTOM_1 = int(match.group(8))
235 return (defaults.WIDTH, defaults.HEIGHT,
236 defaults.X, defaults.Y,
237 defaults.SPLITTER_TOP_0, defaults.SPLITTER_TOP_1,
238 defaults.SPLITTER_BOTTOM_0, defaults.SPLITTER_BOTTOM_1)
240 def get_geom():
241 return '%dx%d+%d,%d %d,%d %d,%d' % (
242 defaults.WIDTH, defaults.HEIGHT,
243 defaults.X, defaults.Y,
244 defaults.SPLITTER_TOP_0, defaults.SPLITTER_TOP_1,
245 defaults.SPLITTER_BOTTOM_0, defaults.SPLITTER_BOTTOM_1)
247 def project_name():
248 return os.path.basename(defaults.DIRECTORY)
250 def slurp(path):
251 file = open(path)
252 slushy = file.read()
253 file.close()
254 return slushy
256 def write(path, contents):
257 file = open(path, 'w')
258 file.write(contents)
259 file.close()
262 class DiffParser(object):
263 def __init__(self, model,
264 filename='', cached=True):
265 self.__header_pattern = re.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
266 self.__headers = []
268 self.__idx = -1
269 self.__diffs = []
270 self.__diff_spans = []
271 self.__diff_offsets = []
273 self.start = None
274 self.end = None
275 self.offset = None
276 self.diffs = []
277 self.selected = []
279 (header, diff) = \
280 model.diff(filename=filename, with_diff_header=True,
281 cached=cached, reverse=cached)
283 self.model = model
284 self.diff = diff
285 self.header = header
286 self.parse_diff(diff)
288 # Always index into the non-reversed diff
289 self.fwd_header, self.fwd_diff = \
290 model.diff(filename=filename, with_diff_header=True,
291 cached=cached, reverse=False)
293 def write_diff(self,filename,which,selected=False,noop=False):
294 if not noop and which < len(self.diffs):
295 diff = self.diffs[which]
296 write(filename, self.header + os.linesep + diff + os.linesep)
297 return True
298 else:
299 return False
301 def get_diffs(self):
302 return self.__diffs
304 def get_diff_subset(self, diff, start, end):
305 newdiff = []
306 diffguts = os.linesep.join(self.__diffs[diff])
308 offset = self.__diff_spans[diff][0]
310 local_offset = 0
312 adds = 0
313 deletes = 0
315 for line in self.__diffs[diff]:
317 line_start = offset + local_offset
318 local_offset += len(line) + 1
319 line_end = offset + local_offset
321 # |line1 |line2 |line3|
322 # |selection----|
323 # '-start '-end
325 # selection has head of diff (line3)
326 if start < line_start and end > line_start and end < line_end:
327 newdiff.append(line)
328 if line.startswith('+'):
329 adds += 1
330 if line.startswith('-'):
331 deletes += 1
332 # selection has all of diff (line2)
333 elif start <= line_start and end >= line_end:
334 newdiff.append(line)
335 if line.startswith('+'):
336 adds += 1
337 if line.startswith('-'):
338 deletes += 1
339 # selection has tail of diff (line1)
340 elif start >= line_start and start < line_end - 1:
341 newdiff.append(line)
342 if line.startswith('+'):
343 adds += 1
344 if line.startswith('-'):
345 deletes += 1
346 else:
347 # Don't add new lines unless selected
348 if line.startswith('+'):
349 continue
350 elif line.startswith('-'):
351 # Don't remove lines unless selected
352 newdiff.append(' ' + line[1:])
353 else:
354 newdiff.append(line)
356 new_count = self.__headers[diff][1] + adds - deletes
358 if new_count != self.__headers[diff][3]:
359 header = '@@ -%d,%d +%d,%d @@' % (
360 self.__headers[diff][0],
361 self.__headers[diff][1],
362 self.__headers[diff][2],
363 new_count)
364 newdiff[0] = header
366 return (self.header
367 + os.linesep
368 + os.linesep.join(newdiff)
369 + os.linesep)
371 def get_spans(self):
372 return self.__diff_spans
374 def get_offsets(self):
375 return self.__diff_offsets
377 def set_diff_to_offset(self, offset):
378 self.offset = offset
379 self.diffs, self.selected = self.get_diff_for_offset(offset)
381 def set_diffs_to_range(self, start, end):
382 self.start = start
383 self.end = end
384 self.diffs, self.selected = self.get_diffs_for_range(start,end)
386 def get_diff_for_offset(self, offset):
387 for idx, diff_offset in enumerate(self.__diff_offsets):
388 if offset < diff_offset:
389 return ([os.linesep.join(self.__diffs[idx])],
390 [idx])
391 return ([],[])
393 def get_diffs_for_range(self, start, end):
394 diffs = []
395 indices = []
396 for idx, span in enumerate(self.__diff_spans):
397 has_end_of_diff = start >= span[0] and start < span[1]
398 has_all_of_diff = start <= span[0] and end >= span[1]
399 has_head_of_diff = end >= span[0] and end <= span[1]
401 selected_diff =(has_end_of_diff
402 or has_all_of_diff
403 or has_head_of_diff)
405 if selected_diff:
406 diff = os.linesep.join(self.__diffs[idx])
407 diffs.append(diff)
408 indices.append(idx)
409 return diffs, indices
411 def parse_diff(self, diff):
412 total_offset = 0
413 self.__idx = -1
414 self.__headers = []
416 for idx, line in enumerate(diff.splitlines()):
418 match = self.__header_pattern.match(line)
419 if match:
420 self.__headers.append([
421 int(match.group(1)),
422 int(match.group(2)),
423 int(match.group(3)),
424 int(match.group(4))
427 self.__diffs.append( [line] )
429 line_len = len(line) + 1
430 self.__diff_spans.append([total_offset,
431 total_offset + line_len])
433 total_offset += line_len
434 self.__diff_offsets.append(total_offset)
436 self.__idx += 1
437 else:
438 if self.__idx < 0:
439 errmsg = 'Malformed diff?\n\n%s' % diff
440 raise AssertionError, errmsg
442 line_len = len(line) + 1
443 total_offset += line_len
445 self.__diffs[self.__idx].append(line)
446 self.__diff_spans[-1][-1] += line_len
447 self.__diff_offsets[self.__idx] += line_len
449 def process_diff_selection(self, selected, offset, selection):
450 if selection:
451 start = self.fwd_diff.index(selection)
452 end = start + len(selection)
453 self.set_diffs_to_range(start, end)
454 else:
455 self.set_diff_to_offset(offset)
456 selected = False
458 # Process diff selection only
459 if selected:
460 for idx in self.selected:
461 contents = self.get_diff_subset(idx, start, end)
462 if contents:
463 tmpfile = get_tmp_filename()
464 write(tmpfile, contents)
465 self.model.apply_diff(tmpfile)
466 os.unlink(tmpfile)
467 # Process a complete hunk
468 else:
469 for idx, diff in enumerate(self.diffs):
470 tmpfile = get_tmp_filename()
471 if self.write_diff(tmpfile,idx):
472 self.model.apply_diff(tmpfile)
473 os.unlink(tmpfile)