doc: Update copyright
[git-cola.git] / cola / utils.py
blob63eb47214e99fc6d609ba534e288fe6d545a37c5
1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous utility functions."""
4 import os
5 import re
6 import sys
7 import errno
8 import platform
9 import subprocess
10 import hashlib
11 import mimetypes
13 from glob import glob
14 from cStringIO import StringIO
16 from cola import git
17 from cola import core
18 from cola import resources
19 from cola.git import shell_quote
21 KNOWN_FILE_MIME_TYPES = {
22 'text': 'script.png',
23 'image': 'image.png',
24 'python': 'script.png',
25 'ruby': 'script.png',
26 'shell': 'script.png',
27 'perl': 'script.png',
28 'octet': 'binary.png',
31 KNOWN_FILE_EXTENSION = {
32 '.java': 'script.png',
33 '.groovy': 'script.png',
34 '.cpp': 'script.png',
35 '.c': 'script.png',
36 '.h': 'script.png',
37 '.cxx': 'script.png',
41 def add_parents(path_entry_set):
42 """Iterate over each item in the set and add its parent directories."""
43 for path in list(path_entry_set):
44 while '//' in path:
45 path = path.replace('//', '/')
46 if path not in path_entry_set:
47 path_entry_set.add(path)
48 if '/' in path:
49 parent_dir = dirname(path)
50 while parent_dir and parent_dir not in path_entry_set:
51 path_entry_set.add(parent_dir)
52 parent_dir = dirname(parent_dir)
53 return path_entry_set
56 def run_cmd(command):
57 """
58 Run arguments as a command and return output.
60 >>> run_cmd(["echo", "hello", "world"])
61 'hello world'
63 """
64 return git.Git.execute(command)
67 def ident_file_type(filename):
68 """Returns an icon based on the contents of filename."""
69 if os.path.exists(filename):
70 filemimetype = mimetypes.guess_type(filename)
71 if filemimetype[0] != None:
72 for filetype, iconname in KNOWN_FILE_MIME_TYPES.iteritems():
73 if filetype in filemimetype[0].lower():
74 return iconname
75 filename = filename.lower()
76 for fileext, iconname in KNOWN_FILE_EXTENSION.iteritems():
77 if filename.endswith(fileext):
78 return iconname
79 return 'generic.png'
80 else:
81 return 'removed.png'
82 # Fallback for modified files of an unknown type
83 return 'generic.png'
86 def file_icon(filename):
87 """
88 Returns the full path to an icon file corresponding to
89 filename"s contents.
90 """
91 return resources.icon(ident_file_type(filename))
94 def win32_abspath(exe):
95 """Return the absolute path to an .exe if it exists"""
96 if os.path.exists(exe):
97 return exe
98 if not exe.endswith('.exe'):
99 exe += '.exe'
100 if os.path.exists(exe):
101 return exe
102 for path in os.environ['PATH'].split(os.pathsep):
103 abspath = os.path.join(path, exe)
104 if os.path.exists(abspath):
105 return abspath
106 return None
109 def win32_expand_paths(args):
110 """Expand filenames after the double-dash"""
111 if '--' not in args:
112 return args
113 dashes_idx = args.index('--')
114 cmd = args[:dashes_idx+1]
115 for path in args[dashes_idx+1:]:
116 cmd.append(shell_quote(os.path.join(os.getcwd(), path)))
117 return cmd
120 def fork(args):
121 """Launch a command in the background."""
122 if is_win32():
123 # Windows is absolutely insane.
125 # If we want to launch 'gitk' we have to use the 'sh -c' trick.
127 # If we want to launch 'git.exe' we have to expand all filenames
128 # after the double-dash.
130 # os.spawnv wants an absolute path in the command name but not in
131 # the command vector. Wow.
132 enc_args = win32_expand_paths([core.encode(a) for a in args])
133 abspath = win32_abspath(enc_args[0])
134 if abspath:
135 # e.g. fork(['git', 'difftool', '--no-prompt', '--', 'path'])
136 return os.spawnv(os.P_NOWAIT, abspath, enc_args)
138 # e.g. fork(['gitk', '--all'])
139 sh_exe = win32_abspath('sh')
140 enc_argv = map(shell_quote, enc_args)
141 cmdstr = ' '.join(enc_argv)
142 cmd = ['sh.exe', '-c', cmdstr]
143 return os.spawnv(os.P_NOWAIT, sh_exe, cmd)
144 else:
145 # Unix is absolutely simple
146 enc_args = [core.encode(a) for a in args]
147 enc_argv = map(shell_quote, enc_args)
148 cmdstr = ' '.join(enc_argv)
149 return os.system(cmdstr + '&')
152 def sublist(a,b):
153 """Subtracts list b from list a and returns the resulting list."""
154 # conceptually, c = a - b
155 c = []
156 for item in a:
157 if item not in b:
158 c.append(item)
159 return c
162 __grep_cache = {}
163 def grep(pattern, items, squash=True):
164 """Greps a list for items that match a pattern and return a list of
165 matching items. If only one item matches, return just that item.
167 isdict = type(items) is dict
168 if pattern in __grep_cache:
169 regex = __grep_cache[pattern]
170 else:
171 regex = __grep_cache[pattern] = re.compile(pattern)
172 matched = []
173 matchdict = {}
174 for item in items:
175 match = regex.match(item)
176 if not match:
177 continue
178 groups = match.groups()
179 if not groups:
180 subitems = match.group(0)
181 else:
182 if len(groups) == 1:
183 subitems = groups[0]
184 else:
185 subitems = list(groups)
186 if isdict:
187 matchdict[item] = items[item]
188 else:
189 matched.append(subitems)
191 if isdict:
192 return matchdict
193 else:
194 if squash and len(matched) == 1:
195 return matched[0]
196 else:
197 return matched
200 def basename(path):
202 An os.path.basename() implementation that always uses '/'
204 Avoid os.path.basename because git's output always
205 uses '/' regardless of platform.
208 return path.rsplit('/', 1)[-1]
211 def dirname(path):
213 An os.path.dirname() implementation that always uses '/'
215 Avoid os.path.dirname because git's output always
216 uses '/' regardless of platform.
219 while '//' in path:
220 path = path.replace('//', '/')
221 path_dirname = path.rsplit('/', 1)[0]
222 if path_dirname == path:
223 return ''
224 return path.rsplit('/', 1)[0]
227 def slurp(path):
228 """Slurps a filepath into a string."""
229 fh = open(path)
230 slushy = core.read_nointr(fh)
231 fh.close()
232 return core.decode(slushy)
235 def write(path, contents):
236 """Writes a string to a file."""
237 fh = open(path, 'w')
238 core.write_nointr(fh, core.encode(contents))
239 fh.close()
241 def strip_prefix(prefix, string):
242 """Return string, without the prefix. Blow up if string doesn't
243 start with prefix."""
244 assert string.startswith(prefix)
245 return string[len(prefix):]
247 def sanitize(s):
248 """Removes shell metacharacters from a string."""
249 for c in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
250 s = s.replace(c, '_')
251 return s
253 def is_linux():
254 """Is this a linux machine?"""
255 while True:
256 try:
257 return platform.system() == 'Linux'
258 except IOError, e:
259 if e.errno == errno.EINTR:
260 continue
261 raise e
263 def is_debian():
264 """Is it debian?"""
265 return os.path.exists('/usr/bin/apt-get')
268 def is_darwin():
269 """Return True on OSX."""
270 while True:
271 try:
272 p = platform.platform()
273 break
274 except IOError, e:
275 if e.errno == errno.EINTR:
276 continue
277 raise e
278 p = p.lower()
279 return 'macintosh' in p or 'darwin' in p
282 _is_win32 = None
283 def is_win32():
284 """Return True on win32"""
285 global _is_win32
286 if _is_win32 is None:
287 _is_win32 = os.name in ('nt', 'dos')
288 return _is_win32
291 def is_broken():
292 """Is it windows or mac? (e.g. is running git-mergetool non-trivial?)"""
293 if is_darwin():
294 return True
295 while True:
296 try:
297 return platform.system() == 'Windows'
298 except IOError, e:
299 if e.errno == errno.EINTR:
300 continue
301 raise e
304 def checksum(path):
305 """Return a cheap md5 hexdigest for a path."""
306 md5 = hashlib.new('md5')
307 md5.update(slurp(path))
308 return md5.hexdigest()
311 def quote_repopath(repopath):
312 """Quote a path for nt/dos only."""
313 if is_win32():
314 repopath = '"%s"' % repopath
315 return repopath
317 # From git.git
318 """Misc. useful functionality used by the rest of this package.
320 This module provides common functionality used by the other modules in
321 this package.
324 # Whether or not to show debug messages
325 DEBUG = False
327 def notify(msg, *args):
328 """Print a message to stderr."""
329 print >> sys.stderr, msg % args
331 def debug (msg, *args):
332 """Print a debug message to stderr when DEBUG is enabled."""
333 if DEBUG:
334 print >> sys.stderr, msg % args
336 def error (msg, *args):
337 """Print an error message to stderr."""
338 print >> sys.stderr, "ERROR:", msg % args
340 def warn(msg, *args):
341 """Print a warning message to stderr."""
342 print >> sys.stderr, "warning:", msg % args
344 def die (msg, *args):
345 """Print as error message to stderr and exit the program."""
346 error(msg, *args)
347 sys.exit(1)
350 class ProgressIndicator(object):
352 """Simple progress indicator.
354 Displayed as a spinning character by default, but can be customized
355 by passing custom messages that overrides the spinning character.
359 States = ("|", "/", "-", "\\")
361 def __init__ (self, prefix = "", f = sys.stdout):
362 """Create a new ProgressIndicator, bound to the given file object."""
363 self.n = 0 # Simple progress counter
364 self.f = f # Progress is written to this file object
365 self.prev_len = 0 # Length of previous msg (to be overwritten)
366 self.prefix = prefix # Prefix prepended to each progress message
367 self.prefix_lens = [] # Stack of prefix string lengths
369 def pushprefix (self, prefix):
370 """Append the given prefix onto the prefix stack."""
371 self.prefix_lens.append(len(self.prefix))
372 self.prefix += prefix
374 def popprefix (self):
375 """Remove the last prefix from the prefix stack."""
376 prev_len = self.prefix_lens.pop()
377 self.prefix = self.prefix[:prev_len]
379 def __call__ (self, msg = None, lf = False):
380 """Indicate progress, possibly with a custom message."""
381 if msg is None:
382 msg = self.States[self.n % len(self.States)]
383 msg = self.prefix + msg
384 print >> self.f, "\r%-*s" % (self.prev_len, msg),
385 self.prev_len = len(msg.expandtabs())
386 if lf:
387 print >> self.f
388 self.prev_len = 0
389 self.n += 1
391 def finish (self, msg = "done", noprefix = False):
392 """Finalize progress indication with the given message."""
393 if noprefix:
394 self.prefix = ""
395 self(msg, True)
398 def start_command (args, cwd = None, shell = False, add_env = None,
399 stdin = subprocess.PIPE, stdout = subprocess.PIPE,
400 stderr = subprocess.PIPE):
401 """Start the given command, and return a subprocess object.
403 This provides a simpler interface to the subprocess module.
406 env = None
407 if add_env is not None:
408 env = os.environ.copy()
409 env.update(add_env)
410 return subprocess.Popen(args, bufsize = 1, stdin = stdin, stdout = stdout,
411 stderr = stderr, cwd = cwd, shell = shell,
412 env = env, universal_newlines = True)
415 def run_command (args, cwd = None, shell = False, add_env = None,
416 flag_error = True):
417 """Run the given command to completion, and return its results.
419 This provides a simpler interface to the subprocess module.
421 The results are formatted as a 3-tuple: (exit_code, output, errors)
423 If flag_error is enabled, Error messages will be produced if the
424 subprocess terminated with a non-zero exit code and/or stderr
425 output.
427 The other arguments are passed on to start_command().
430 process = start_command(args, cwd, shell, add_env)
431 (output, errors) = process.communicate()
432 exit_code = process.returncode
433 if flag_error and errors:
434 error("'%s' returned errors:\n---\n%s---", " ".join(args), errors)
435 if flag_error and exit_code:
436 error("'%s' returned exit code %i", " ".join(args), exit_code)
437 return (exit_code, output, errors)