cola: add more documentation strings to the cola modules
[git-cola.git] / cola / utils.py
blobc3dab35f6b25d7310fb409f7996056a744e9db48
1 #!/usr/bin/env python
2 # Copyright (c) 2008 David Aguilar
3 """This module provides miscellaneous utility functions."""
5 import os
6 import re
7 import sys
8 import platform
9 import subprocess
10 from glob import glob
11 from cStringIO import StringIO
13 from cola import defaults
14 from cola import git
15 from cola.git import shell_quote
16 from cola.core import encode, decode
17 from cola.exception import ColaException
19 PREFIX = os.path.realpath(os.path.dirname(os.path.dirname(sys.argv[0])))
20 QMDIR = os.path.join(PREFIX, 'share', 'cola', 'qm')
21 ICONSDIR = os.path.join(PREFIX, 'share', 'cola', 'icons')
22 STYLEDIR = os.path.join(PREFIX, 'share', 'cola', 'styles')
23 DOCDIR = os.path.join(PREFIX, 'share', 'doc', 'cola')
25 KNOWN_FILE_TYPES = {
26 'ascii c': 'c.png',
27 'python': 'script.png',
28 'ruby': 'script.png',
29 'shell': 'script.png',
30 'perl': 'script.png',
31 'java': 'script.png',
32 'assembler': 'binary.png',
33 'binary': 'binary.png',
34 'byte': 'binary.png',
35 'image': 'image.png',
38 def run_cmd(*command):
39 """
40 Runs a *command argument list and returns the output.
41 e.g. run_cmd("echo", "hello", "world")
42 """
43 return git.Git.execute(command)
45 def get_qm_for_locale(locale):
46 """Returns the .qm file for a particular $LANG values."""
47 regex = re.compile(r'([^\.])+\..*$')
48 match = regex.match(locale)
49 if match:
50 locale = match.group(1)
52 basename = locale.split('_')[0]
54 return os.path.join(QMDIR, basename +'.qm')
56 def get_resource_dirs(styledir):
57 """Returns all directories underneath the share/cola/styles directory."""
58 return [ r for r in glob(styledir+ '/*') if os.path.isdir(r) ]
60 def get_stylesheet(name):
61 """Returns the path to a stylesheet within the cola install tree."""
62 stylesheet = os.path.join(STYLEDIR, name+'.qss')
63 if os.path.exists(stylesheet):
64 return stylesheet
65 else:
66 return None
68 def get_htmldocs():
69 """Returns the path to the cola html documentation."""
70 return os.path.join(DOCDIR, 'git-cola.html')
72 def ident_file_type(filename):
73 """Returns an icon based on the contents of filename."""
74 if os.path.exists(filename):
75 fileinfo = run_cmd('file','-b',filename)
76 for filetype, iconname in KNOWN_FILE_TYPES.iteritems():
77 if filetype in fileinfo.lower():
78 return iconname
79 else:
80 return 'removed.png'
81 # Fallback for modified files of an unknown type
82 return 'generic.png'
84 def get_file_icon(filename):
85 """
86 Returns the full path to an icon file corresponding to
87 filename"s contents.
88 """
89 icon_file = ident_file_type(filename)
90 return get_icon(icon_file)
92 def get_icon(icon_file):
93 """Returns the full path to an icon file given a basename."""
94 return os.path.join(ICONSDIR, icon_file)
96 def fork(*args):
97 """Launches a command in the background."""
98 args = tuple([ encode(a) for a in args ])
99 if os.name in ('nt', 'dos'):
100 for path in os.environ['PATH'].split(os.pathsep):
101 filename = os.path.join(path, args[0]) + ".exe"
102 if os.path.exists(filename):
103 try:
104 return os.spawnv(os.P_NOWAIT, filename, args)
105 except os.error:
106 pass
107 raise IOError('cannot find executable: %s' % program)
108 else:
109 argv = map(shell_quote, args)
110 return os.system(' '.join(argv) + '&')
112 def sublist(a,b):
113 """Subtracts list b from list a and returns the resulting list."""
114 # conceptually, c = a - b
115 c = []
116 for item in a:
117 if item not in b:
118 c.append(item)
119 return c
121 __grep_cache = {}
122 def grep(pattern, items, squash=True):
123 """Greps a list for items that match a pattern and return a list of
124 matching items. If only one item matches, return just that item.
126 isdict = type(items) is dict
127 if pattern in __grep_cache:
128 regex = __grep_cache[pattern]
129 else:
130 regex = __grep_cache[pattern] = re.compile(pattern)
131 matched = []
132 matchdict = {}
133 for item in items:
134 match = regex.match(item)
135 if not match: continue
136 groups = match.groups()
137 if not groups:
138 subitems = match.group(0)
139 else:
140 if len(groups) == 1:
141 subitems = groups[0]
142 else:
143 subitems = list(groups)
144 if isdict:
145 matchdict[item] = items[item]
146 else:
147 matched.append(subitems)
149 if isdict:
150 return matchdict
151 else:
152 if squash and len(matched) == 1:
153 return matched[0]
154 else:
155 return matched
157 def basename(path):
158 """Avoid os.path.basename because we are explicitly
159 parsing git"s output, which contains /"s regardless
160 of platform (a.t.m.)
162 base_regex = re.compile('(.*?/)?([^/]+)$')
163 match = base_regex.match(path)
164 if match:
165 return match.group(2)
166 else:
167 return pathstr
169 HEADER_LENGTH = 80
170 def header(msg):
171 pad = HEADER_LENGTH - len(msg) - 4 # len(':+') + len('+:')
172 extra = pad % 2
173 pad /= 2
174 return(':+'
175 +(' ' * pad)
176 + msg
177 +(' ' * (pad + extra))
178 + '+:'
179 + '\n')
181 def parse_geom(geomstr):
182 regex = re.compile('^(\d+)x(\d+)\+(\d+),(\d+).*?')
183 match = regex.match(geomstr)
184 if match:
185 defaults.WIDTH = int(match.group(1))
186 defaults.HEIGHT = int(match.group(2))
187 defaults.X = int(match.group(3))
188 defaults.Y = int(match.group(4))
189 return (defaults.WIDTH, defaults.HEIGHT, defaults.X, defaults.Y)
191 def get_geom():
192 return ('%dx%d+%d,%d'
193 % (defaults.WIDTH, defaults.HEIGHT, defaults.X, defaults.Y))
195 def project_name():
196 return os.path.basename(defaults.DIRECTORY)
198 def slurp(path):
199 """Slurps a filepath into a string."""
200 file = open(path)
201 slushy = file.read()
202 file.close()
203 return decode(slushy)
205 def write(path, contents):
206 """Writes a string to a file."""
207 file = open(path, 'w')
208 file.write(encode(contents))
209 file.close()
211 class DiffParser(object):
212 """Handles parsing diff for use by the interactive index editor."""
213 def __init__(self, model, filename='',
214 cached=True, branch=None, reverse=False):
216 self.__header_re = re.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
217 self.__headers = []
219 self.__idx = -1
220 self.__diffs = []
221 self.__diff_spans = []
222 self.__diff_offsets = []
224 self.start = None
225 self.end = None
226 self.offset = None
227 self.diffs = []
228 self.selected = []
230 (header, diff) = model.diff_helper(filename=filename,
231 branch=branch,
232 with_diff_header=True,
233 cached=cached and not bool(branch),
234 reverse=cached or bool(branch) or reverse)
235 self.model = model
236 self.diff = diff
237 self.header = header
238 self.parse_diff(diff)
240 # Always index into the non-reversed diff
241 self.fwd_header, self.fwd_diff = \
242 model.diff_helper(filename=filename,
243 branch=branch,
244 with_diff_header=True,
245 cached=cached and not bool(branch),
246 reverse=bool(branch))
248 def write_diff(self,filename,which,selected=False,noop=False):
249 """Writes a new diff corresponding to the user's selection."""
250 if not noop and which < len(self.diffs):
251 diff = self.diffs[which]
252 write(filename, self.header + '\n' + diff + '\n')
253 return True
254 else:
255 return False
257 def get_diffs(self):
258 """Returns the list of diffs."""
259 return self.__diffs
261 def get_diff_subset(self, diff, start, end):
262 """Processes the diffs and returns a selected subset from that diff.
264 adds = 0
265 deletes = 0
266 newdiff = []
267 local_offset = 0
268 offset = self.__diff_spans[diff][0]
269 diffguts = '\n'.join(self.__diffs[diff])
271 for line in self.__diffs[diff]:
272 line_start = offset + local_offset
273 local_offset += len(line) + 1 #\n
274 line_end = offset + local_offset
275 # |line1 |line2 |line3 |
276 # |--selection--|
277 # '-start '-end
278 # selection has head of diff (line3)
279 if start < line_start and end > line_start and end < line_end:
280 newdiff.append(line)
281 if line.startswith('+'):
282 adds += 1
283 if line.startswith('-'):
284 deletes += 1
285 # selection has all of diff (line2)
286 elif start <= line_start and end >= line_end:
287 newdiff.append(line)
288 if line.startswith('+'):
289 adds += 1
290 if line.startswith('-'):
291 deletes += 1
292 # selection has tail of diff (line1)
293 elif start >= line_start and start < line_end - 1:
294 newdiff.append(line)
295 if line.startswith('+'):
296 adds += 1
297 if line.startswith('-'):
298 deletes += 1
299 else:
300 # Don't add new lines unless selected
301 if line.startswith('+'):
302 continue
303 elif line.startswith('-'):
304 # Don't remove lines unless selected
305 newdiff.append(' ' + line[1:])
306 else:
307 newdiff.append(line)
309 new_count = self.__headers[diff][1] + adds - deletes
310 if new_count != self.__headers[diff][3]:
311 header = '@@ -%d,%d +%d,%d @@' % (
312 self.__headers[diff][0],
313 self.__headers[diff][1],
314 self.__headers[diff][2],
315 new_count)
316 newdiff[0] = header
318 return (self.header + '\n' + '\n'.join(newdiff) + '\n')
320 def get_spans(self):
321 """Returns the line spans of each hunk."""
322 return self.__diff_spans
324 def get_offsets(self):
325 """Returns the offsets."""
326 return self.__diff_offsets
328 def set_diff_to_offset(self, offset):
329 """Sets the diff selection to be the hunk at a particular offset."""
330 self.offset = offset
331 self.diffs, self.selected = self.get_diff_for_offset(offset)
333 def set_diffs_to_range(self, start, end):
334 """Sets the diff selection to be a range of hunks."""
335 self.start = start
336 self.end = end
337 self.diffs, self.selected = self.get_diffs_for_range(start,end)
339 def get_diff_for_offset(self, offset):
340 """Returns the hunks for a particular offset."""
341 for idx, diff_offset in enumerate(self.__diff_offsets):
342 if offset < diff_offset:
343 return (['\n'.join(self.__diffs[idx])], [idx])
344 return ([],[])
346 def get_diffs_for_range(self, start, end):
347 """Returns the hunks for a selected range."""
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 """Parses a diff and extracts headers, offsets, hunks, etc.
367 total_offset = 0
368 self.__idx = -1
369 self.__headers = []
371 for idx, line in enumerate(diff.splitlines()):
372 match = self.__header_re.match(line)
373 if match:
374 self.__headers.append([
375 int(match.group(1)),
376 int(match.group(2)),
377 int(match.group(3)),
378 int(match.group(4))
380 self.__diffs.append( [line] )
382 line_len = len(line) + 1 #\n
383 self.__diff_spans.append([total_offset,
384 total_offset + line_len])
385 total_offset += line_len
386 self.__diff_offsets.append(total_offset)
387 self.__idx += 1
388 else:
389 if self.__idx < 0:
390 errmsg = 'Malformed diff?\n\n%s' % diff
391 raise AssertionError, errmsg
392 line_len = len(line) + 1
393 total_offset += line_len
395 self.__diffs[self.__idx].append(line)
396 self.__diff_spans[-1][-1] += line_len
397 self.__diff_offsets[self.__idx] += line_len
399 def process_diff_selection(self, selected, offset, selection,
400 apply_to_worktree=False):
401 """Processes a diff selection and applies changes to the work tree
402 or index."""
403 if selection:
404 start = self.fwd_diff.index(selection)
405 end = start + len(selection)
406 self.set_diffs_to_range(start, end)
407 else:
408 self.set_diff_to_offset(offset)
409 selected = False
410 # Process diff selection only
411 if selected:
412 for idx in self.selected:
413 contents = self.get_diff_subset(idx, start, end)
414 if contents:
415 tmpfile = self.model.get_tmp_filename()
416 write(tmpfile, contents)
417 if apply_to_worktree:
418 self.model.apply_diff_to_worktree(tmpfile)
419 else:
420 self.model.apply_diff(tmpfile)
421 os.unlink(tmpfile)
422 # Process a complete hunk
423 else:
424 for idx, diff in enumerate(self.diffs):
425 tmpfile = self.model.get_tmp_filename()
426 if self.write_diff(tmpfile,idx):
427 if apply_to_worktree:
428 self.model.apply_diff_to_worktree(tmpfile)
429 else:
430 self.model.apply_diff(tmpfile)
431 os.unlink(tmpfile)
433 def strip_prefix(prefix, string):
434 """Return string, without the prefix. Blow up if string doesn't
435 start with prefix."""
436 assert string.startswith(prefix)
437 return string[len(prefix):]
439 def sanitize_input(input):
440 """Removes shell metacharacters from a string."""
441 for c in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
442 input = input.replace(c, '_')
443 return input
445 def is_linux():
446 """Is this a linux machine?"""
447 return platform.system() == 'Linux'
449 def is_debian():
450 """Is it debian?"""
451 return os.path.exists('/usr/bin/apt-get')
453 def is_broken():
454 """Is it windows or mac? (e.g. is running git-mergetool non-trivial?)"""
455 return (platform.system() == 'Windows'
456 or 'Macintosh' in platform.platform())