ui: add fetch/push/pull buttons and a spacer
[ugit.git] / ugit / utils.py
blobb30f107acbc00a2685f4a1796aac9bf645443ea5
1 #!/usr/bin/env python
2 import sys
3 import os
4 import re
5 import subprocess
6 from cStringIO import StringIO
8 import defaults
10 PREFIX = os.path.realpath(os.path.dirname(os.path.dirname(sys.argv[0])))
11 QMDIR = os.path.join(PREFIX, 'share', 'ugit', 'qm')
12 ICONSDIR = os.path.join(PREFIX, 'share', 'ugit', 'icons')
13 KNOWN_FILE_TYPES = {
14 'ascii c': 'c.png',
15 'python': 'script.png',
16 'ruby': 'script.png',
17 'shell': 'script.png',
18 'perl': 'script.png',
19 'java': 'script.png',
20 'assembler': 'binary.png',
21 'binary': 'binary.png',
22 'byte': 'binary.png',
23 'image': 'image.png',
26 def run_cmd(*command):
27 """Runs a *command argument list and returns the output.
28 e.g. run_cmd("echo", "hello", "world")
29 """
30 # Start the process
31 proc = subprocess.Popen(command, stdout = subprocess.PIPE)
33 # Wait for the process to return
34 stdout_value = proc.stdout.read()
35 proc.stdout.close()
36 proc.wait()
37 status = proc.poll()
39 # Strip off trailing whitespace by default
40 return stdout_value.rstrip()
43 def get_qm_for_locale(locale):
44 regex = re.compile(r'([^\.])+\..*$')
45 match = regex.match(locale)
46 if match:
47 locale = match.group(1)
49 basename = locale.split('_')[0]
51 return os.path.join(QMDIR, basename +'.qm')
53 def ident_file_type(filename):
54 '''Returns an icon based on the contents of filename.'''
55 if os.path.exists(filename):
56 fileinfo = run_cmd('file','-b',filename)
57 for filetype, iconname in KNOWN_FILE_TYPES.iteritems():
58 if filetype in fileinfo.lower():
59 return iconname
60 else:
61 return 'removed.png'
62 # Fallback for modified files of an unknown type
63 return 'generic.png'
65 def get_file_icon(filename):
66 '''Returns the full path to an icon file corresponding to
67 filename's contents.'''
68 icon_file = ident_file_type(filename)
69 return get_icon(icon_file)
71 def get_icon(icon_file):
72 return os.path.join(ICONSDIR, icon_file)
74 def fork(*args):
75 return subprocess.Popen(args).pid
77 # c = a - b
78 def sublist(a,b):
79 c = []
80 for item in a:
81 if item not in b:
82 c.append(item)
83 return c
85 __grep_cache = {}
86 def grep(pattern, items, squash=True):
87 isdict = type(items) is dict
88 if pattern in __grep_cache:
89 regex = __grep_cache[pattern]
90 else:
91 regex = __grep_cache[pattern] = re.compile(pattern)
92 matched = []
93 matchdict = {}
94 for item in items:
95 match = regex.match(item)
96 if not match: continue
97 groups = match.groups()
98 if not groups:
99 subitems = match.group(0)
100 else:
101 if len(groups) == 1:
102 subitems = groups[0]
103 else:
104 subitems = list(groups)
105 if isdict:
106 matchdict[item] = items[item]
107 else:
108 matched.append(subitems)
110 if isdict:
111 return matchdict
112 else:
113 if squash and len(matched) == 1:
114 return matched[0]
115 else:
116 return matched
118 def basename(path):
119 '''Avoid os.path.basename because we are explicitly
120 parsing git's output, which contains /'s regardless
121 of platform (a.t.m.)
123 base_regex = re.compile('(.*?/)?([^/]+)$')
124 match = base_regex.match(path)
125 if match:
126 return match.group(2)
127 else:
128 return pathstr
130 def shell_quote(*inputs):
131 '''Quote strings so that they can be suitably martialled
132 off to the shell. This method supports POSIX sh syntax.
133 This is crucial to properly handle command line arguments
134 with spaces, quotes, double-quotes, etc.'''
136 regex = re.compile('[^\w!%+,\-./:@^]')
137 quote_regex = re.compile("((?:'\\''){2,})")
139 ret = []
140 for input in inputs:
141 if not input:
142 continue
144 if '\x00' in input:
145 raise AssertionError,('No way to quote strings '
146 'containing null(\\000) bytes')
148 # = does need quoting else in command position it's a
149 # program-local environment setting
150 match = regex.search(input)
151 if match and '=' not in input:
152 # ' -> '\''
153 input = input.replace("'", "'\\''")
155 # make multiple ' in a row look simpler
156 # '\'''\'''\'' -> '"'''"'
157 quote_match = quote_regex.match(input)
158 if quote_match:
159 quotes = match.group(1)
160 input.replace(quotes,
161 ("'" *(len(quotes)/4)) + "\"'")
163 input = "'%s'" % input
164 if input.startswith("''"):
165 input = input[2:]
167 if input.endswith("''"):
168 input = input[:-2]
169 ret.append(input)
170 return ' '.join(ret)
172 HEADER_LENGTH = 80
173 def header(msg):
174 pad = HEADER_LENGTH - len(msg) - 4 # len(':+') + len('+:')
175 extra = pad % 2
176 pad /= 2
177 return(':+'
178 +(' ' * pad)
179 + msg
180 +(' ' *(pad + extra))
181 + '+:'
182 + '\n')
184 def parse_geom(geomstr):
185 regex = re.compile('^(\d+)x(\d+)\+(\d+),(\d+)')
186 match = regex.match(geomstr)
187 if match:
188 defaults.WIDTH = int(match.group(1))
189 defaults.HEIGHT = int(match.group(2))
190 defaults.X = int(match.group(3))
191 defaults.Y = int(match.group(4))
193 return (defaults.WIDTH, defaults.HEIGHT,
194 defaults.X, defaults.Y)
196 def get_geom():
197 return '%dx%d+%d,%d' % (
198 defaults.WIDTH,
199 defaults.HEIGHT,
200 defaults.X,
201 defaults.Y,
204 def project_name():
205 return os.path.basename(defaults.DIRECTORY)
207 def slurp(path):
208 file = open(path)
209 slushy = file.read()
210 file.close()
211 return slushy
213 def write(path, contents):
214 file = open(path, 'w')
215 file.write(contents)
216 file.close()
218 class DiffParser(object):
219 def __init__( self, model,
220 filename='',
221 cached=True
223 self.__header_re = re.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
224 self.__headers = []
226 self.__idx = -1
227 self.__diffs = []
228 self.__diff_spans = []
229 self.__diff_offsets = []
231 self.start = None
232 self.end = None
233 self.offset = None
234 self.diffs = []
235 self.selected = []
237 (header, diff) = \
238 model.diff_helper(
239 filename=filename,
240 with_diff_header=True,
241 cached=cached,
242 reverse=cached)
244 self.model = model
245 self.diff = diff
246 self.header = header
247 self.parse_diff(diff)
249 # Always index into the non-reversed diff
250 self.fwd_header, self.fwd_diff = \
251 model.diff_helper(
252 filename=filename,
253 with_diff_header=True,
254 cached=cached,
255 reverse=False,
258 def write_diff(self,filename,which,selected=False,noop=False):
259 if not noop and which < len(self.diffs):
260 diff = self.diffs[which]
261 write(filename, self.header + '\n' + diff + '\n')
262 return True
263 else:
264 return False
266 def get_diffs(self):
267 return self.__diffs
269 def get_diff_subset(self, diff, start, end):
270 adds = 0
271 deletes = 0
272 newdiff = []
273 local_offset = 0
274 offset = self.__diff_spans[diff][0]
275 diffguts = '\n'.join(self.__diffs[diff])
277 for line in self.__diffs[diff]:
278 line_start = offset + local_offset
279 local_offset += len(line) + 1 #\n
280 line_end = offset + local_offset
281 # |line1 |line2 |line3 |
282 # |--selection--|
283 # '-start '-end
284 # selection has head of diff (line3)
285 if start < line_start and end > line_start and end < line_end:
286 newdiff.append(line)
287 if line.startswith('+'):
288 adds += 1
289 if line.startswith('-'):
290 deletes += 1
291 # selection has all of diff (line2)
292 elif start <= line_start and end >= line_end:
293 newdiff.append(line)
294 if line.startswith('+'):
295 adds += 1
296 if line.startswith('-'):
297 deletes += 1
298 # selection has tail of diff (line1)
299 elif start >= line_start and start < line_end - 1:
300 newdiff.append(line)
301 if line.startswith('+'):
302 adds += 1
303 if line.startswith('-'):
304 deletes += 1
305 else:
306 # Don't add new lines unless selected
307 if line.startswith('+'):
308 continue
309 elif line.startswith('-'):
310 # Don't remove lines unless selected
311 newdiff.append(' ' + line[1:])
312 else:
313 newdiff.append(line)
315 new_count = self.__headers[diff][1] + adds - deletes
316 if new_count != self.__headers[diff][3]:
317 header = '@@ -%d,%d +%d,%d @@' % (
318 self.__headers[diff][0],
319 self.__headers[diff][1],
320 self.__headers[diff][2],
321 new_count)
322 newdiff[0] = header
324 return (self.header + '\n' + '\n'.join(newdiff) + '\n')
326 def get_spans(self):
327 return self.__diff_spans
329 def get_offsets(self):
330 return self.__diff_offsets
332 def set_diff_to_offset(self, offset):
333 self.offset = offset
334 self.diffs, self.selected = self.get_diff_for_offset(offset)
336 def set_diffs_to_range(self, start, end):
337 self.start = start
338 self.end = end
339 self.diffs, self.selected = self.get_diffs_for_range(start,end)
341 def get_diff_for_offset(self, offset):
342 for idx, diff_offset in enumerate(self.__diff_offsets):
343 if offset < diff_offset:
344 return (['\n'.join(self.__diffs[idx])], [idx])
345 return ([],[])
347 def get_diffs_for_range(self, start, end):
348 diffs = []
349 indices = []
350 for idx, span in enumerate(self.__diff_spans):
351 has_end_of_diff = start >= span[0] and start < span[1]
352 has_all_of_diff = start <= span[0] and end >= span[1]
353 has_head_of_diff = end >= span[0] and end <= span[1]
355 selected_diff =(has_end_of_diff
356 or has_all_of_diff
357 or has_head_of_diff)
358 if selected_diff:
359 diff = '\n'.join(self.__diffs[idx])
360 diffs.append(diff)
361 indices.append(idx)
362 return diffs, indices
364 def parse_diff(self, diff):
365 total_offset = 0
366 self.__idx = -1
367 self.__headers = []
369 for idx, line in enumerate(diff.splitlines()):
370 match = self.__header_re.match(line)
371 if match:
372 self.__headers.append([
373 int(match.group(1)),
374 int(match.group(2)),
375 int(match.group(3)),
376 int(match.group(4))
378 self.__diffs.append( [line] )
380 line_len = len(line) + 1 #\n
381 self.__diff_spans.append([total_offset,
382 total_offset + line_len])
383 total_offset += line_len
384 self.__diff_offsets.append(total_offset)
385 self.__idx += 1
386 else:
387 if self.__idx < 0:
388 errmsg = 'Malformed diff?\n\n%s' % diff
389 raise AssertionError, errmsg
390 line_len = len(line) + 1
391 total_offset += line_len
393 self.__diffs[self.__idx].append(line)
394 self.__diff_spans[-1][-1] += line_len
395 self.__diff_offsets[self.__idx] += line_len
397 def process_diff_selection(self, selected, offset, selection):
398 if selection:
399 start = self.fwd_diff.index(selection)
400 end = start + len(selection)
401 self.set_diffs_to_range(start, end)
402 else:
403 self.set_diff_to_offset(offset)
404 selected = False
405 # Process diff selection only
406 if selected:
407 for idx in self.selected:
408 contents = self.get_diff_subset(idx, start, end)
409 if contents:
410 tmpfile = self.model.get_tmp_filename()
411 write(tmpfile, contents)
412 self.model.apply_diff(tmpfile)
413 os.unlink(tmpfile)
414 # Process a complete hunk
415 else:
416 for idx, diff in enumerate(self.diffs):
417 tmpfile = self.model.get_tmp_filename()
418 if self.write_diff(tmpfile,idx):
419 self.model.apply_diff(tmpfile)
420 os.unlink(tmpfile)
422 def sanitize_input(input):
423 for c in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
424 input = input.replace(c, '_')
425 return input