configure: match git's shell script style
[ugit.git] / ugitlibs / utils.py
blob6d863575c5f3b103c3980e807e5172fad609e432
1 #!/usr/bin/env python
2 import sys
3 import os
4 import re
5 import time
6 from cStringIO import StringIO
7 from PyQt4.QtCore import QProcess
9 import defaults
11 PREFIX = os.path.realpath(os.path.dirname(os.path.dirname(sys.argv[0])))
12 QMDIR = os.path.join(PREFIX, 'share', 'ugit', 'qm')
13 ICONSDIR = os.path.join(PREFIX, 'share', 'ugit', 'icons')
14 KNOWN_FILE_TYPES = {
15 'ascii c': 'c.png',
16 'python': 'script.png',
17 'ruby': 'script.png',
18 'shell': 'script.png',
19 'perl': 'script.png',
20 'java': 'script.png',
21 'assembler': 'binary.png',
22 'binary': 'binary.png',
23 'byte': 'binary.png',
24 'image': 'image.png',
27 def get_qm_for_locale(locale):
28 regex = re.compile(r'([^\.])+\..*$')
29 match = regex.match(locale)
30 if match:
31 locale = match.group(1)
33 basename = locale.split('_')[0]
35 return os.path.join(QMDIR, basename +'.qm')
37 def ident_file_type(filename):
38 '''Returns an icon based on the contents of filename.'''
39 if os.path.exists(filename):
40 fileinfo = run_cmd('file','-b',filename)
41 for filetype, iconname in KNOWN_FILE_TYPES.iteritems():
42 if filetype in fileinfo.lower():
43 return iconname
44 else:
45 return 'removed.png'
46 # Fallback for modified files of an unknown type
47 return 'generic.png'
49 def get_file_icon(filename):
50 '''Returns the full path to an icon file corresponding to
51 filename's contents.'''
52 icon_file = ident_file_type(filename)
53 return get_icon(icon_file)
55 def get_icon(icon_file):
56 return os.path.join(ICONSDIR, icon_file)
58 def pop_key(d, key):
59 val = d.get(key)
60 try: del d[key]
61 except: pass
62 return val
64 def run_cmd(cmd, *args, **kwargs):
65 """
66 Returns an array of strings from the command's output.
67 defaults:
68 raw: off -> passing raw=True returns a string instead of a list of strings.
69 with_status: off -> passing with_status=True returns
70 tuple(status,output) instead of just output
72 run_command(foo fi, bar, buzz, baz=value, q=None)
73 Produces:
74 argv=[foo fi -q --baz=value bar buzz]
76 run_command
77 """
78 raw = pop_key(kwargs, 'raw')
79 with_status = pop_key(kwargs,'with_status')
80 with_stderr = not pop_key(kwargs,'without_stderr')
81 kwarglist = []
82 for k,v in kwargs.iteritems():
83 if len(k) > 1:
84 if v is True:
85 kwarglist.append("--%s" % k)
86 elif v is not None and type(v) is not bool:
87 kwarglist.append("--%s=%s" % (k,v))
88 else:
89 if v is True:
90 kwarglist.append("-%s" % k)
91 elif v is not None and type(v) is not bool:
92 kwarglist.append("-%s" % k)
93 kwarglist.append(str(v))
94 # Handle cmd as either a string or an argv list
95 if type(cmd) is str:
96 # we only call run_cmd(str) with str='git command'
97 # or other simple commands
98 cmd = cmd.split(' ')
99 cmd += kwarglist
100 cmd += list(args)
101 else:
102 cmd = list(cmd + kwarglist + list(args))
104 child = QProcess()
105 if with_stderr:
106 child.setProcessChannelMode(QProcess.MergedChannels);
108 child.start(cmd[0], cmd[1:])
109 if not (child.waitForStarted() and child.waitForFinished()):
110 return ''
111 output = str(child.readAll())
112 # run_cmd('cmd', *args, raw=True) if we want the full, raw output
113 if raw:
114 if with_status:
115 return (child.exitCode(), output)
116 else:
117 return output
118 else:
119 # simplify parsing by removing trailing \n from commands
120 if with_status:
121 return child.exitCode(), output[:-1]
122 else:
123 return output[:-1]
125 def fork(*argv):
126 pid = os.fork()
127 if pid: return
128 os.execlp(*argv)
130 # c = a - b
131 def sublist(a,b):
132 c = []
133 for item in a:
134 if item not in b:
135 c.append(item)
136 return c
138 __grep_cache = {}
139 def grep(pattern, items, squash=True):
140 isdict = type(items) is dict
141 if pattern in __grep_cache:
142 regex = __grep_cache[pattern]
143 else:
144 regex = __grep_cache[pattern] = re.compile(pattern)
145 matched = []
146 matchdict = {}
147 for item in items:
148 match = regex.match(item)
149 if not match: continue
150 groups = match.groups()
151 if not groups:
152 subitems = match.group(0)
153 else:
154 if len(groups) == 1:
155 subitems = groups[0]
156 else:
157 subitems = list(groups)
158 if isdict:
159 matchdict[item] = items[item]
160 else:
161 matched.append(subitems)
163 if isdict:
164 return matchdict
165 else:
166 if squash and len(matched) == 1:
167 return matched[0]
168 else:
169 return matched
171 def basename(path):
172 '''Avoid os.path.basename because we are explicitly
173 parsing git's output, which contains /'s regardless
174 of platform (a.t.m.)
176 base_regex = re.compile('(.*?/)?([^/]+)$')
177 match = base_regex.match(path)
178 if match:
179 return match.group(2)
180 else:
181 return pathstr
183 def shell_quote(*inputs):
184 '''Quote strings so that they can be suitably martialled
185 off to the shell. This method supports POSIX sh syntax.
186 This is crucial to properly handle command line arguments
187 with spaces, quotes, double-quotes, etc.'''
189 regex = re.compile('[^\w!%+,\-./:@^]')
190 quote_regex = re.compile("((?:'\\''){2,})")
192 ret = []
193 for input in inputs:
194 if not input:
195 continue
197 if '\x00' in input:
198 raise AssertionError,('No way to quote strings '
199 'containing null(\\000) bytes')
201 # = does need quoting else in command position it's a
202 # program-local environment setting
203 match = regex.search(input)
204 if match and '=' not in input:
205 # ' -> '\''
206 input = input.replace("'", "'\\''")
208 # make multiple ' in a row look simpler
209 # '\'''\'''\'' -> '"'''"'
210 quote_match = quote_regex.match(input)
211 if quote_match:
212 quotes = match.group(1)
213 input.replace(quotes,
214 ("'" *(len(quotes)/4)) + "\"'")
216 input = "'%s'" % input
217 if input.startswith("''"):
218 input = input[2:]
220 if input.endswith("''"):
221 input = input[:-2]
222 ret.append(input)
223 return ' '.join(ret)
225 def get_tmp_filename():
226 # Allow TMPDIR/TMP with a fallback to /tmp
227 return '.ugit.%s.%s' %( os.getpid(), time.time() )
229 HEADER_LENGTH = 80
230 def header(msg):
231 pad = HEADER_LENGTH - len(msg) - 4 # len(':+') + len('+:')
232 extra = pad % 2
233 pad /= 2
234 return(':+'
235 +(' ' * pad)
236 + msg
237 +(' ' *(pad + extra))
238 + '+:'
239 + '\n')
241 def parse_geom(geomstr):
242 regex = re.compile('^(\d+)x(\d+)\+(\d+),(\d+) (\d+),(\d+) (\d+),(\d+)')
243 match = regex.match(geomstr)
244 if match:
245 defaults.WIDTH = int(match.group(1))
246 defaults.HEIGHT = int(match.group(2))
247 defaults.X = int(match.group(3))
248 defaults.Y = int(match.group(4))
249 defaults.SPLITTER_TOP_0 = int(match.group(5))
250 defaults.SPLITTER_TOP_1 = int(match.group(6))
251 defaults.SPLITTER_BOTTOM_0 = int(match.group(7))
252 defaults.SPLITTER_BOTTOM_1 = int(match.group(8))
254 return (defaults.WIDTH, defaults.HEIGHT,
255 defaults.X, defaults.Y,
256 defaults.SPLITTER_TOP_0, defaults.SPLITTER_TOP_1,
257 defaults.SPLITTER_BOTTOM_0, defaults.SPLITTER_BOTTOM_1)
259 def get_geom():
260 return '%dx%d+%d,%d %d,%d %d,%d' % (
261 defaults.WIDTH, defaults.HEIGHT,
262 defaults.X, defaults.Y,
263 defaults.SPLITTER_TOP_0, defaults.SPLITTER_TOP_1,
264 defaults.SPLITTER_BOTTOM_0, defaults.SPLITTER_BOTTOM_1)
266 def project_name():
267 return os.path.basename(defaults.DIRECTORY)
269 def slurp(path):
270 file = open(path)
271 slushy = file.read()
272 file.close()
273 return slushy
275 def write(path, contents):
276 file = open(path, 'w')
277 file.write(contents)
278 file.close()
280 class DiffParser(object):
281 def __init__(self, model,
282 filename='', cached=True):
283 self.__header_pattern = re.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
284 self.__headers = []
286 self.__idx = -1
287 self.__diffs = []
288 self.__diff_spans = []
289 self.__diff_offsets = []
291 self.start = None
292 self.end = None
293 self.offset = None
294 self.diffs = []
295 self.selected = []
297 (header, diff) = \
298 model.diff(filename=filename, with_diff_header=True,
299 cached=cached, reverse=cached)
301 self.model = model
302 self.diff = diff
303 self.header = header
304 self.parse_diff(diff)
306 # Always index into the non-reversed diff
307 self.fwd_header, self.fwd_diff = \
308 model.diff(filename=filename, with_diff_header=True,
309 cached=cached, reverse=False)
311 def write_diff(self,filename,which,selected=False,noop=False):
312 if not noop and which < len(self.diffs):
313 diff = self.diffs[which]
314 write(filename, self.header + '\n' + diff + '\n')
315 return True
316 else:
317 return False
319 def get_diffs(self):
320 return self.__diffs
322 def get_diff_subset(self, diff, start, end):
323 adds = 0
324 deletes = 0
325 newdiff = []
326 local_offset = 0
327 offset = self.__diff_spans[diff][0]
328 diffguts = '\n'.join(self.__diffs[diff])
330 for line in self.__diffs[diff]:
331 line_start = offset + local_offset
332 local_offset += len(line) + 1 #\n
333 line_end = offset + local_offset
334 # |line1 |line2 |line3 |
335 # |--selection--|
336 # '-start '-end
337 # selection has head of diff (line3)
338 if start < line_start and end > line_start and end < line_end:
339 newdiff.append(line)
340 if line.startswith('+'):
341 adds += 1
342 if line.startswith('-'):
343 deletes += 1
344 # selection has all of diff (line2)
345 elif start <= line_start and end >= line_end:
346 newdiff.append(line)
347 if line.startswith('+'):
348 adds += 1
349 if line.startswith('-'):
350 deletes += 1
351 # selection has tail of diff (line1)
352 elif start >= line_start and start < line_end - 1:
353 newdiff.append(line)
354 if line.startswith('+'):
355 adds += 1
356 if line.startswith('-'):
357 deletes += 1
358 else:
359 # Don't add new lines unless selected
360 if line.startswith('+'):
361 continue
362 elif line.startswith('-'):
363 # Don't remove lines unless selected
364 newdiff.append(' ' + line[1:])
365 else:
366 newdiff.append(line)
368 new_count = self.__headers[diff][1] + adds - deletes
369 if new_count != self.__headers[diff][3]:
370 header = '@@ -%d,%d +%d,%d @@' % (
371 self.__headers[diff][0],
372 self.__headers[diff][1],
373 self.__headers[diff][2],
374 new_count)
375 newdiff[0] = header
377 return (self.header + '\n' + '\n'.join(newdiff) + '\n')
379 def get_spans(self):
380 return self.__diff_spans
382 def get_offsets(self):
383 return self.__diff_offsets
385 def set_diff_to_offset(self, offset):
386 self.offset = offset
387 self.diffs, self.selected = self.get_diff_for_offset(offset)
389 def set_diffs_to_range(self, start, end):
390 self.start = start
391 self.end = end
392 self.diffs, self.selected = self.get_diffs_for_range(start,end)
394 def get_diff_for_offset(self, offset):
395 for idx, diff_offset in enumerate(self.__diff_offsets):
396 if offset < diff_offset:
397 return (['\n'.join(self.__diffs[idx])], [idx])
398 return ([],[])
400 def get_diffs_for_range(self, start, end):
401 diffs = []
402 indices = []
403 for idx, span in enumerate(self.__diff_spans):
404 has_end_of_diff = start >= span[0] and start < span[1]
405 has_all_of_diff = start <= span[0] and end >= span[1]
406 has_head_of_diff = end >= span[0] and end <= span[1]
408 selected_diff =(has_end_of_diff
409 or has_all_of_diff
410 or has_head_of_diff)
411 if selected_diff:
412 diff = '\n'.join(self.__diffs[idx])
413 diffs.append(diff)
414 indices.append(idx)
415 return diffs, indices
417 def parse_diff(self, diff):
418 total_offset = 0
419 self.__idx = -1
420 self.__headers = []
422 for idx, line in enumerate(diff.splitlines()):
423 match = self.__header_pattern.match(line)
424 if match:
425 self.__headers.append([
426 int(match.group(1)),
427 int(match.group(2)),
428 int(match.group(3)),
429 int(match.group(4))
431 self.__diffs.append( [line] )
433 line_len = len(line) + 1 #\n
434 self.__diff_spans.append([total_offset,
435 total_offset + line_len])
436 total_offset += line_len
437 self.__diff_offsets.append(total_offset)
438 self.__idx += 1
439 else:
440 if self.__idx < 0:
441 errmsg = 'Malformed diff?\n\n%s' % diff
442 raise AssertionError, errmsg
443 line_len = len(line) + 1
444 total_offset += line_len
446 self.__diffs[self.__idx].append(line)
447 self.__diff_spans[-1][-1] += line_len
448 self.__diff_offsets[self.__idx] += line_len
450 def process_diff_selection(self, selected, offset, selection):
451 if selection:
452 start = self.fwd_diff.index(selection)
453 end = start + len(selection)
454 self.set_diffs_to_range(start, end)
455 else:
456 self.set_diff_to_offset(offset)
457 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)