oops: fixed git reset usage
[ugit.git] / ugit / utils.py
blob1cb33c852bcb3ec32feb7e720736b7cd859adf70
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("git foo", bar, buzz, baz=value, q=True)
73 implies:
74 argv=["git","foo","-q","--baz=value","bar","buzz"]
75 """
76 raw = pop_key(kwargs, 'raw')
77 with_status = pop_key(kwargs,'with_status')
78 with_stderr = not pop_key(kwargs,'without_stderr')
79 kwarglist = []
80 for k,v in kwargs.iteritems():
81 if len(k) > 1:
82 k = k.replace('_','-')
83 if v is True:
84 kwarglist.append("--%s" % k)
85 elif v is not None and type(v) is not bool:
86 kwarglist.append("--%s=%s" % (k,v))
87 else:
88 if v is True:
89 kwarglist.append("-%s" % k)
90 elif v is not None and type(v) is not bool:
91 kwarglist.append("-%s" % k)
92 kwarglist.append(str(v))
93 # Handle cmd as either a string or an argv list
94 if type(cmd) is str:
95 # we only call run_cmd(str) with str='git command'
96 # or other simple commands
97 cmd = cmd.split(' ')
98 cmd += kwarglist
99 cmd += list(args)
100 else:
101 cmd = list(cmd + kwarglist + list(args))
103 child = QProcess()
104 if with_stderr:
105 child.setProcessChannelMode(QProcess.MergedChannels);
107 child.start(cmd[0], cmd[1:])
108 if not (child.waitForStarted() and child.waitForFinished()):
109 return ''
110 output = str(child.readAll())
111 # run_cmd('cmd', *args, raw=True) if we want the full, raw output
112 if raw:
113 if with_status:
114 return (child.exitCode(), output)
115 else:
116 return output
117 else:
118 # simplify parsing by removing trailing \n from commands
119 if with_status:
120 return child.exitCode(), output[:-1]
121 else:
122 return output[:-1]
124 def fork(*argv):
125 pid = os.fork()
126 if pid: return
127 os.execlp(*argv)
129 # c = a - b
130 def sublist(a,b):
131 c = []
132 for item in a:
133 if item not in b:
134 c.append(item)
135 return c
137 __grep_cache = {}
138 def grep(pattern, items, squash=True):
139 isdict = type(items) is dict
140 if pattern in __grep_cache:
141 regex = __grep_cache[pattern]
142 else:
143 regex = __grep_cache[pattern] = re.compile(pattern)
144 matched = []
145 matchdict = {}
146 for item in items:
147 match = regex.match(item)
148 if not match: continue
149 groups = match.groups()
150 if not groups:
151 subitems = match.group(0)
152 else:
153 if len(groups) == 1:
154 subitems = groups[0]
155 else:
156 subitems = list(groups)
157 if isdict:
158 matchdict[item] = items[item]
159 else:
160 matched.append(subitems)
162 if isdict:
163 return matchdict
164 else:
165 if squash and len(matched) == 1:
166 return matched[0]
167 else:
168 return matched
170 def basename(path):
171 '''Avoid os.path.basename because we are explicitly
172 parsing git's output, which contains /'s regardless
173 of platform (a.t.m.)
175 base_regex = re.compile('(.*?/)?([^/]+)$')
176 match = base_regex.match(path)
177 if match:
178 return match.group(2)
179 else:
180 return pathstr
182 def shell_quote(*inputs):
183 '''Quote strings so that they can be suitably martialled
184 off to the shell. This method supports POSIX sh syntax.
185 This is crucial to properly handle command line arguments
186 with spaces, quotes, double-quotes, etc.'''
188 regex = re.compile('[^\w!%+,\-./:@^]')
189 quote_regex = re.compile("((?:'\\''){2,})")
191 ret = []
192 for input in inputs:
193 if not input:
194 continue
196 if '\x00' in input:
197 raise AssertionError,('No way to quote strings '
198 'containing null(\\000) bytes')
200 # = does need quoting else in command position it's a
201 # program-local environment setting
202 match = regex.search(input)
203 if match and '=' not in input:
204 # ' -> '\''
205 input = input.replace("'", "'\\''")
207 # make multiple ' in a row look simpler
208 # '\'''\'''\'' -> '"'''"'
209 quote_match = quote_regex.match(input)
210 if quote_match:
211 quotes = match.group(1)
212 input.replace(quotes,
213 ("'" *(len(quotes)/4)) + "\"'")
215 input = "'%s'" % input
216 if input.startswith("''"):
217 input = input[2:]
219 if input.endswith("''"):
220 input = input[:-2]
221 ret.append(input)
222 return ' '.join(ret)
224 def get_tmp_filename():
225 # Allow TMPDIR/TMP with a fallback to /tmp
226 return '.ugit.%s.%s' %( os.getpid(), time.time() )
228 HEADER_LENGTH = 80
229 def header(msg):
230 pad = HEADER_LENGTH - len(msg) - 4 # len(':+') + len('+:')
231 extra = pad % 2
232 pad /= 2
233 return(':+'
234 +(' ' * pad)
235 + msg
236 +(' ' *(pad + extra))
237 + '+:'
238 + '\n')
240 def parse_geom(geomstr):
241 regex = re.compile('^(\d+)x(\d+)\+(\d+),(\d+) (\d+),(\d+) (\d+),(\d+)')
242 match = regex.match(geomstr)
243 if match:
244 defaults.WIDTH = int(match.group(1))
245 defaults.HEIGHT = int(match.group(2))
246 defaults.X = int(match.group(3))
247 defaults.Y = int(match.group(4))
248 defaults.SPLITTER_TOP_0 = int(match.group(5))
249 defaults.SPLITTER_TOP_1 = int(match.group(6))
250 defaults.SPLITTER_BOTTOM_0 = int(match.group(7))
251 defaults.SPLITTER_BOTTOM_1 = int(match.group(8))
253 return (defaults.WIDTH, defaults.HEIGHT,
254 defaults.X, defaults.Y,
255 defaults.SPLITTER_TOP_0, defaults.SPLITTER_TOP_1,
256 defaults.SPLITTER_BOTTOM_0, defaults.SPLITTER_BOTTOM_1)
258 def get_geom():
259 return '%dx%d+%d,%d %d,%d %d,%d' % (
260 defaults.WIDTH, defaults.HEIGHT,
261 defaults.X, defaults.Y,
262 defaults.SPLITTER_TOP_0, defaults.SPLITTER_TOP_1,
263 defaults.SPLITTER_BOTTOM_0, defaults.SPLITTER_BOTTOM_1)
265 def project_name():
266 return os.path.basename(defaults.DIRECTORY)
268 def slurp(path):
269 file = open(path)
270 slushy = file.read()
271 file.close()
272 return slushy
274 def write(path, contents):
275 file = open(path, 'w')
276 file.write(contents)
277 file.close()
279 class DiffParser(object):
280 def __init__(self, model,
281 filename='', cached=True):
282 self.__header_pattern = re.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
283 self.__headers = []
285 self.__idx = -1
286 self.__diffs = []
287 self.__diff_spans = []
288 self.__diff_offsets = []
290 self.start = None
291 self.end = None
292 self.offset = None
293 self.diffs = []
294 self.selected = []
296 (header, diff) = \
297 model.diff_helper(
298 filename=filename,
299 with_diff_header=True,
300 cached=cached,
301 reverse=cached)
303 self.model = model
304 self.diff = diff
305 self.header = header
306 self.parse_diff(diff)
308 # Always index into the non-reversed diff
309 self.fwd_header, self.fwd_diff = \
310 model.diff_helper(
311 filename=filename,
312 with_diff_header=True,
313 cached=cached,
314 reverse=False,
317 def write_diff(self,filename,which,selected=False,noop=False):
318 if not noop and which < len(self.diffs):
319 diff = self.diffs[which]
320 write(filename, self.header + '\n' + diff + '\n')
321 return True
322 else:
323 return False
325 def get_diffs(self):
326 return self.__diffs
328 def get_diff_subset(self, diff, start, end):
329 adds = 0
330 deletes = 0
331 newdiff = []
332 local_offset = 0
333 offset = self.__diff_spans[diff][0]
334 diffguts = '\n'.join(self.__diffs[diff])
336 for line in self.__diffs[diff]:
337 line_start = offset + local_offset
338 local_offset += len(line) + 1 #\n
339 line_end = offset + local_offset
340 # |line1 |line2 |line3 |
341 # |--selection--|
342 # '-start '-end
343 # selection has head of diff (line3)
344 if start < line_start and end > line_start and end < line_end:
345 newdiff.append(line)
346 if line.startswith('+'):
347 adds += 1
348 if line.startswith('-'):
349 deletes += 1
350 # selection has all of diff (line2)
351 elif start <= line_start and end >= line_end:
352 newdiff.append(line)
353 if line.startswith('+'):
354 adds += 1
355 if line.startswith('-'):
356 deletes += 1
357 # selection has tail of diff (line1)
358 elif start >= line_start and start < line_end - 1:
359 newdiff.append(line)
360 if line.startswith('+'):
361 adds += 1
362 if line.startswith('-'):
363 deletes += 1
364 else:
365 # Don't add new lines unless selected
366 if line.startswith('+'):
367 continue
368 elif line.startswith('-'):
369 # Don't remove lines unless selected
370 newdiff.append(' ' + line[1:])
371 else:
372 newdiff.append(line)
374 new_count = self.__headers[diff][1] + adds - deletes
375 if new_count != self.__headers[diff][3]:
376 header = '@@ -%d,%d +%d,%d @@' % (
377 self.__headers[diff][0],
378 self.__headers[diff][1],
379 self.__headers[diff][2],
380 new_count)
381 newdiff[0] = header
383 return (self.header + '\n' + '\n'.join(newdiff) + '\n')
385 def get_spans(self):
386 return self.__diff_spans
388 def get_offsets(self):
389 return self.__diff_offsets
391 def set_diff_to_offset(self, offset):
392 self.offset = offset
393 self.diffs, self.selected = self.get_diff_for_offset(offset)
395 def set_diffs_to_range(self, start, end):
396 self.start = start
397 self.end = end
398 self.diffs, self.selected = self.get_diffs_for_range(start,end)
400 def get_diff_for_offset(self, offset):
401 for idx, diff_offset in enumerate(self.__diff_offsets):
402 if offset < diff_offset:
403 return (['\n'.join(self.__diffs[idx])], [idx])
404 return ([],[])
406 def get_diffs_for_range(self, start, end):
407 diffs = []
408 indices = []
409 for idx, span in enumerate(self.__diff_spans):
410 has_end_of_diff = start >= span[0] and start < span[1]
411 has_all_of_diff = start <= span[0] and end >= span[1]
412 has_head_of_diff = end >= span[0] and end <= span[1]
414 selected_diff =(has_end_of_diff
415 or has_all_of_diff
416 or has_head_of_diff)
417 if selected_diff:
418 diff = '\n'.join(self.__diffs[idx])
419 diffs.append(diff)
420 indices.append(idx)
421 return diffs, indices
423 def parse_diff(self, diff):
424 total_offset = 0
425 self.__idx = -1
426 self.__headers = []
428 for idx, line in enumerate(diff.splitlines()):
429 match = self.__header_pattern.match(line)
430 if match:
431 self.__headers.append([
432 int(match.group(1)),
433 int(match.group(2)),
434 int(match.group(3)),
435 int(match.group(4))
437 self.__diffs.append( [line] )
439 line_len = len(line) + 1 #\n
440 self.__diff_spans.append([total_offset,
441 total_offset + line_len])
442 total_offset += line_len
443 self.__diff_offsets.append(total_offset)
444 self.__idx += 1
445 else:
446 if self.__idx < 0:
447 errmsg = 'Malformed diff?\n\n%s' % diff
448 raise AssertionError, errmsg
449 line_len = len(line) + 1
450 total_offset += line_len
452 self.__diffs[self.__idx].append(line)
453 self.__diff_spans[-1][-1] += line_len
454 self.__diff_offsets[self.__idx] += line_len
456 def process_diff_selection(self, selected, offset, selection):
457 if selection:
458 start = self.fwd_diff.index(selection)
459 end = start + len(selection)
460 self.set_diffs_to_range(start, end)
461 else:
462 self.set_diff_to_offset(offset)
463 selected = False
464 # Process diff selection only
465 if selected:
466 for idx in self.selected:
467 contents = self.get_diff_subset(idx, start, end)
468 if contents:
469 tmpfile = get_tmp_filename()
470 write(tmpfile, contents)
471 self.model.apply_diff(tmpfile)
472 os.unlink(tmpfile)
473 # Process a complete hunk
474 else:
475 for idx, diff in enumerate(self.diffs):
476 tmpfile = get_tmp_filename()
477 if self.write_diff(tmpfile,idx):
478 self.model.apply_diff(tmpfile)
479 os.unlink(tmpfile)