cola: include GitPython to make installation simpler for users
[git-cola.git] / cola / utils.py
blob6dd3c849e59a70aa0ec738e5ae5e7a21de756b2e
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', 'cola', 'qm')
12 ICONSDIR = os.path.join(PREFIX, 'share', 'cola', '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 """
28 Runs a *command argument list and returns the output.
29 e.g. run_cmd("echo", "hello", "world")
30 """
31 # Start the process
32 proc = subprocess.Popen(command, stdout = subprocess.PIPE)
34 # Wait for the process to return
35 stdout_value = proc.stdout.read()
36 proc.stdout.close()
37 proc.wait()
38 status = proc.poll()
40 # Strip off trailing whitespace by default
41 return stdout_value.rstrip()
44 def get_qm_for_locale(locale):
45 regex = re.compile(r'([^\.])+\..*$')
46 match = regex.match(locale)
47 if match:
48 locale = match.group(1)
50 basename = locale.split('_')[0]
52 return os.path.join(QMDIR, basename +'.qm')
54 def ident_file_type(filename):
55 """Returns an icon based on the contents of filename."""
56 if os.path.exists(filename):
57 fileinfo = run_cmd('file','-b',filename)
58 for filetype, iconname in KNOWN_FILE_TYPES.iteritems():
59 if filetype in fileinfo.lower():
60 return iconname
61 else:
62 return 'removed.png'
63 # Fallback for modified files of an unknown type
64 return 'generic.png'
66 def get_file_icon(filename):
67 """
68 Returns the full path to an icon file corresponding to
69 filename"s contents.
70 """
71 icon_file = ident_file_type(filename)
72 return get_icon(icon_file)
74 def get_icon(icon_file):
75 return os.path.join(ICONSDIR, icon_file)
77 def fork(*args):
78 if os.name in ('nt', 'dos'):
79 for path in os.pathsep.split(os.environ["PATH"]):
80 file = os.path.join(path, args[0]) + ".exe"
81 try:
82 return os.spawnv(os.P_NOWAIT, file, (file,) + args[1:])
83 except os.error:
84 pass
85 raise IOError('cannot find executable: %s' % program)
86 else:
87 argv = map(shell_quote, args)
88 return os.system(' '.join(argv) + '&')
90 # c = a - b
91 def sublist(a,b):
92 c = []
93 for item in a:
94 if item not in b:
95 c.append(item)
96 return c
98 __grep_cache = {}
99 def grep(pattern, items, squash=True):
100 isdict = type(items) is dict
101 if pattern in __grep_cache:
102 regex = __grep_cache[pattern]
103 else:
104 regex = __grep_cache[pattern] = re.compile(pattern)
105 matched = []
106 matchdict = {}
107 for item in items:
108 match = regex.match(item)
109 if not match: continue
110 groups = match.groups()
111 if not groups:
112 subitems = match.group(0)
113 else:
114 if len(groups) == 1:
115 subitems = groups[0]
116 else:
117 subitems = list(groups)
118 if isdict:
119 matchdict[item] = items[item]
120 else:
121 matched.append(subitems)
123 if isdict:
124 return matchdict
125 else:
126 if squash and len(matched) == 1:
127 return matched[0]
128 else:
129 return matched
131 def basename(path):
132 """Avoid os.path.basename because we are explicitly
133 parsing git"s output, which contains /"s regardless
134 of platform (a.t.m.)
136 base_regex = re.compile('(.*?/)?([^/]+)$')
137 match = base_regex.match(path)
138 if match:
139 return match.group(2)
140 else:
141 return pathstr
143 def shell_quote(*inputs):
145 Quote strings so that they can be suitably martialled
146 off to the shell. This method supports POSIX sh syntax.
147 This is crucial to properly handle command line arguments
148 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, ("'" *(len(quotes)/4)) + "\"'")
177 input = "'%s'" % input
178 if input.startswith("''"):
179 input = input[2:]
181 if input.endswith("''"):
182 input = input[:-2]
183 ret.append(input)
184 return ' '.join(ret)
186 HEADER_LENGTH = 80
187 def header(msg):
188 pad = HEADER_LENGTH - len(msg) - 4 # len(':+') + len('+:')
189 extra = pad % 2
190 pad /= 2
191 return(':+'
192 +(' ' * pad)
193 + msg
194 +(' ' * (pad + extra))
195 + '+:'
196 + '\n')
198 def parse_geom(geomstr):
199 regex = re.compile('^(\d+)x(\d+)\+(\d+),(\d+)')
200 match = regex.match(geomstr)
201 if match:
202 defaults.WIDTH = int(match.group(1))
203 defaults.HEIGHT = int(match.group(2))
204 defaults.X = int(match.group(3))
205 defaults.Y = int(match.group(4))
207 return (defaults.WIDTH,
208 defaults.HEIGHT,
209 defaults.X,
210 defaults.Y)
212 def get_geom():
213 return '%dx%d+%d,%d' % (defaults.WIDTH,
214 defaults.HEIGHT,
215 defaults.X,
216 defaults.Y)
218 def project_name():
219 return os.path.basename(defaults.DIRECTORY)
221 def slurp(path):
222 file = open(path)
223 slushy = file.read()
224 file.close()
225 return slushy
227 def write(path, contents):
228 file = open(path, 'w')
229 file.write(contents)
230 file.close()
232 class DiffParser(object):
233 def __init__(self, model, filename='', cached=True, branch=None):
235 self.__header_re = re.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
236 self.__headers = []
238 self.__idx = -1
239 self.__diffs = []
240 self.__diff_spans = []
241 self.__diff_offsets = []
243 self.start = None
244 self.end = None
245 self.offset = None
246 self.diffs = []
247 self.selected = []
249 (header, diff) = model.diff_helper(filename=filename,
250 branch=branch,
251 with_diff_header=True,
252 cached=cached and not bool(branch),
253 reverse=cached or bool(branch))
254 self.model = model
255 self.diff = diff
256 self.header = header
257 self.parse_diff(diff)
259 # Always index into the non-reversed diff
260 self.fwd_header, self.fwd_diff = \
261 model.diff_helper(filename=filename,
262 branch=branch,
263 with_diff_header=True,
264 cached=cached and not bool(branch),
265 reverse=bool(branch))
267 def write_diff(self,filename,which,selected=False,noop=False):
268 if not noop and which < len(self.diffs):
269 diff = self.diffs[which]
270 write(filename, self.header + '\n' + diff + '\n')
271 return True
272 else:
273 return False
275 def get_diffs(self):
276 return self.__diffs
278 def get_diff_subset(self, diff, start, end):
279 adds = 0
280 deletes = 0
281 newdiff = []
282 local_offset = 0
283 offset = self.__diff_spans[diff][0]
284 diffguts = '\n'.join(self.__diffs[diff])
286 for line in self.__diffs[diff]:
287 line_start = offset + local_offset
288 local_offset += len(line) + 1 #\n
289 line_end = offset + local_offset
290 # |line1 |line2 |line3 |
291 # |--selection--|
292 # '-start '-end
293 # selection has head of diff (line3)
294 if start < line_start and end > line_start and end < line_end:
295 newdiff.append(line)
296 if line.startswith('+'):
297 adds += 1
298 if line.startswith('-'):
299 deletes += 1
300 # selection has all of diff (line2)
301 elif start <= line_start and end >= line_end:
302 newdiff.append(line)
303 if line.startswith('+'):
304 adds += 1
305 if line.startswith('-'):
306 deletes += 1
307 # selection has tail of diff (line1)
308 elif start >= line_start and start < line_end - 1:
309 newdiff.append(line)
310 if line.startswith('+'):
311 adds += 1
312 if line.startswith('-'):
313 deletes += 1
314 else:
315 # Don't add new lines unless selected
316 if line.startswith('+'):
317 continue
318 elif line.startswith('-'):
319 # Don't remove lines unless selected
320 newdiff.append(' ' + line[1:])
321 else:
322 newdiff.append(line)
324 new_count = self.__headers[diff][1] + adds - deletes
325 if new_count != self.__headers[diff][3]:
326 header = '@@ -%d,%d +%d,%d @@' % (
327 self.__headers[diff][0],
328 self.__headers[diff][1],
329 self.__headers[diff][2],
330 new_count)
331 newdiff[0] = header
333 return (self.header + '\n' + '\n'.join(newdiff) + '\n')
335 def get_spans(self):
336 return self.__diff_spans
338 def get_offsets(self):
339 return self.__diff_offsets
341 def set_diff_to_offset(self, offset):
342 self.offset = offset
343 self.diffs, self.selected = self.get_diff_for_offset(offset)
345 def set_diffs_to_range(self, start, end):
346 self.start = start
347 self.end = end
348 self.diffs, self.selected = self.get_diffs_for_range(start,end)
350 def get_diff_for_offset(self, offset):
351 for idx, diff_offset in enumerate(self.__diff_offsets):
352 if offset < diff_offset:
353 return (['\n'.join(self.__diffs[idx])], [idx])
354 return ([],[])
356 def get_diffs_for_range(self, start, end):
357 diffs = []
358 indices = []
359 for idx, span in enumerate(self.__diff_spans):
360 has_end_of_diff = start >= span[0] and start < span[1]
361 has_all_of_diff = start <= span[0] and end >= span[1]
362 has_head_of_diff = end >= span[0] and end <= span[1]
364 selected_diff =(has_end_of_diff
365 or has_all_of_diff
366 or has_head_of_diff)
367 if selected_diff:
368 diff = '\n'.join(self.__diffs[idx])
369 diffs.append(diff)
370 indices.append(idx)
371 return diffs, indices
373 def parse_diff(self, diff):
374 total_offset = 0
375 self.__idx = -1
376 self.__headers = []
378 for idx, line in enumerate(diff.splitlines()):
379 match = self.__header_re.match(line)
380 if match:
381 self.__headers.append([
382 int(match.group(1)),
383 int(match.group(2)),
384 int(match.group(3)),
385 int(match.group(4))
387 self.__diffs.append( [line] )
389 line_len = len(line) + 1 #\n
390 self.__diff_spans.append([total_offset,
391 total_offset + line_len])
392 total_offset += line_len
393 self.__diff_offsets.append(total_offset)
394 self.__idx += 1
395 else:
396 if self.__idx < 0:
397 errmsg = 'Malformed diff?\n\n%s' % diff
398 raise AssertionError, errmsg
399 line_len = len(line) + 1
400 total_offset += line_len
402 self.__diffs[self.__idx].append(line)
403 self.__diff_spans[-1][-1] += line_len
404 self.__diff_offsets[self.__idx] += line_len
406 def process_diff_selection(self, selected, offset, selection, branch=False):
407 if selection:
408 start = self.fwd_diff.index(selection)
409 end = start + len(selection)
410 self.set_diffs_to_range(start, end)
411 else:
412 self.set_diff_to_offset(offset)
413 selected = False
414 # Process diff selection only
415 if selected:
416 for idx in self.selected:
417 contents = self.get_diff_subset(idx, start, end)
418 if contents:
419 tmpfile = self.model.get_tmp_filename()
420 write(tmpfile, contents)
421 if branch:
422 self.model.apply_diff_to_worktree(tmpfile)
423 else:
424 self.model.apply_diff(tmpfile)
425 os.unlink(tmpfile)
426 # Process a complete hunk
427 else:
428 for idx, diff in enumerate(self.diffs):
429 tmpfile = self.model.get_tmp_filename()
430 if self.write_diff(tmpfile,idx):
431 if branch:
432 self.model.apply_diff_to_worktree(tmpfile)
433 else:
434 self.model.apply_diff(tmpfile)
435 os.unlink(tmpfile)
437 def sanitize_input(input):
438 for c in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
439 input = input.replace(c, '_')
440 return input