Update release notes for 1.4.2.2
[git-cola.git] / cola / utils.py
blob588e4dd237b025f1ffa2ed3aaf1878c8a5b92c44
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 commands
9 import platform
10 import subprocess
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.compat import hashlib
20 from cola.decorators import memoize
23 KNOWN_FILE_MIME_TYPES = {
24 'text': 'script.png',
25 'image': 'image.png',
26 'python': 'script.png',
27 'ruby': 'script.png',
28 'shell': 'script.png',
29 'perl': 'script.png',
30 'octet': 'binary.png',
33 KNOWN_FILE_EXTENSION = {
34 '.java': 'script.png',
35 '.groovy': 'script.png',
36 '.cpp': 'script.png',
37 '.c': 'script.png',
38 '.h': 'script.png',
39 '.cxx': 'script.png',
43 def add_parents(path_entry_set):
44 """Iterate over each item in the set and add its parent directories."""
45 for path in list(path_entry_set):
46 while '//' in path:
47 path = path.replace('//', '/')
48 if path not in path_entry_set:
49 path_entry_set.add(path)
50 if '/' in path:
51 parent_dir = dirname(path)
52 while parent_dir and parent_dir not in path_entry_set:
53 path_entry_set.add(parent_dir)
54 parent_dir = dirname(parent_dir)
55 return path_entry_set
58 def run_cmd(command):
59 """
60 Run arguments as a command and return output.
62 >>> run_cmd(["echo", "hello", "world"])
63 'hello world'
65 """
66 return git.Git.execute(command)
69 def ident_file_type(filename):
70 """Returns an icon based on the contents of filename."""
71 if os.path.exists(filename):
72 filemimetype = mimetypes.guess_type(filename)
73 if filemimetype[0] != None:
74 for filetype, iconname in KNOWN_FILE_MIME_TYPES.iteritems():
75 if filetype in filemimetype[0].lower():
76 return iconname
77 filename = filename.lower()
78 for fileext, iconname in KNOWN_FILE_EXTENSION.iteritems():
79 if filename.endswith(fileext):
80 return iconname
81 return 'generic.png'
82 else:
83 return 'removed.png'
84 # Fallback for modified files of an unknown type
85 return 'generic.png'
88 def file_icon(filename):
89 """
90 Returns the full path to an icon file corresponding to
91 filename"s contents.
92 """
93 return resources.icon(ident_file_type(filename))
96 def win32_abspath(exe):
97 """Return the absolute path to an .exe if it exists"""
98 if os.path.exists(exe):
99 return exe
100 if not exe.endswith('.exe'):
101 exe += '.exe'
102 if os.path.exists(exe):
103 return exe
104 for path in os.environ['PATH'].split(os.pathsep):
105 abspath = os.path.join(path, exe)
106 if os.path.exists(abspath):
107 return abspath
108 return None
111 def win32_expand_paths(args):
112 """Expand and quote filenames after the double-dash"""
113 if '--' not in args:
114 return args
115 dashes_idx = args.index('--')
116 cmd = args[:dashes_idx+1]
117 for path in args[dashes_idx+1:]:
118 cmd.append(commands.mkarg(os.path.join(os.getcwd(), path)))
119 return cmd
122 def fork(args):
123 """Launch a command in the background."""
124 if is_win32():
125 # Windows is absolutely insane.
127 # If we want to launch 'gitk' we have to use the 'sh -c' trick.
129 # If we want to launch 'git.exe' we have to expand all filenames
130 # after the double-dash.
132 # os.spawnv wants an absolute path in the command name but not in
133 # the command vector. Wow.
134 enc_args = win32_expand_paths([core.encode(a) for a in args])
135 abspath = win32_abspath(enc_args[0])
136 if abspath:
137 # e.g. fork(['git', 'difftool', '--no-prompt', '--', 'path'])
138 return os.spawnv(os.P_NOWAIT, abspath, enc_args)
140 # e.g. fork(['gitk', '--all'])
141 sh_exe = win32_abspath('sh')
142 enc_argv = map(commands.mkarg, enc_args)
143 cmdstr = ' '.join(enc_argv)
144 cmd = ['sh.exe', '-c', cmdstr]
145 return os.spawnv(os.P_NOWAIT, sh_exe, cmd)
146 else:
147 # Unix is absolutely simple
148 enc_args = [core.encode(a) for a in args]
149 enc_argv = map(commands.mkarg, enc_args)
150 cmdstr = ' '.join(enc_argv)
151 return os.system(cmdstr + '&')
154 def sublist(a,b):
155 """Subtracts list b from list a and returns the resulting list."""
156 # conceptually, c = a - b
157 c = []
158 for item in a:
159 if item not in b:
160 c.append(item)
161 return c
164 __grep_cache = {}
165 def grep(pattern, items, squash=True):
166 """Greps a list for items that match a pattern and return a list of
167 matching items. If only one item matches, return just that item.
169 isdict = type(items) is dict
170 if pattern in __grep_cache:
171 regex = __grep_cache[pattern]
172 else:
173 regex = __grep_cache[pattern] = re.compile(pattern)
174 matched = []
175 matchdict = {}
176 for item in items:
177 match = regex.match(item)
178 if not match:
179 continue
180 groups = match.groups()
181 if not groups:
182 subitems = match.group(0)
183 else:
184 if len(groups) == 1:
185 subitems = groups[0]
186 else:
187 subitems = list(groups)
188 if isdict:
189 matchdict[item] = items[item]
190 else:
191 matched.append(subitems)
193 if isdict:
194 return matchdict
195 else:
196 if squash and len(matched) == 1:
197 return matched[0]
198 else:
199 return matched
202 def basename(path):
204 An os.path.basename() implementation that always uses '/'
206 Avoid os.path.basename because git's output always
207 uses '/' regardless of platform.
210 return path.rsplit('/', 1)[-1]
213 def dirname(path):
215 An os.path.dirname() implementation that always uses '/'
217 Avoid os.path.dirname because git's output always
218 uses '/' regardless of platform.
221 while '//' in path:
222 path = path.replace('//', '/')
223 path_dirname = path.rsplit('/', 1)[0]
224 if path_dirname == path:
225 return ''
226 return path.rsplit('/', 1)[0]
229 def slurp(path):
230 """Slurps a filepath into a string."""
231 fh = open(path)
232 slushy = core.read_nointr(fh)
233 fh.close()
234 return core.decode(slushy)
237 def write(path, contents):
238 """Writes a raw string to a file."""
239 fh = open(path, 'wb')
240 core.write_nointr(fh, core.encode(contents))
241 fh.close()
244 def strip_prefix(prefix, string):
245 """Return string, without the prefix. Blow up if string doesn't
246 start with prefix."""
247 assert string.startswith(prefix)
248 return string[len(prefix):]
250 def sanitize(s):
251 """Removes shell metacharacters from a string."""
252 for c in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
253 s = s.replace(c, '_')
254 return s
256 def is_linux():
257 """Is this a linux machine?"""
258 while True:
259 try:
260 return platform.system() == 'Linux'
261 except IOError, e:
262 if e.errno == errno.EINTR:
263 continue
264 raise e
266 def is_debian():
267 """Is it debian?"""
268 return os.path.exists('/usr/bin/apt-get')
271 def is_darwin():
272 """Return True on OSX."""
273 while True:
274 try:
275 p = platform.platform()
276 break
277 except IOError, e:
278 if e.errno == errno.EINTR:
279 continue
280 raise e
281 p = p.lower()
282 return 'macintosh' in p or 'darwin' in p
285 @memoize
286 def is_win32():
287 """Return True on win32"""
288 return os.name in ('nt', 'dos')
291 @memoize
292 def is_broken():
293 """Is it windows or mac? (e.g. is running git-mergetool non-trivial?)"""
294 if is_darwin():
295 return True
296 while True:
297 try:
298 return platform.system() == 'Windows'
299 except IOError, e:
300 if e.errno == errno.EINTR:
301 continue
302 raise e
303 return False
306 def checksum(path):
307 """Return a cheap md5 hexdigest for a path."""
308 md5 = hashlib.new('md5')
309 md5.update(slurp(path))
310 return md5.hexdigest()
313 def quote_repopath(repopath):
314 """Quote a path for nt/dos only."""
315 if is_win32():
316 repopath = '"%s"' % repopath
317 return repopath
319 # From git.git
320 """Misc. useful functionality used by the rest of this package.
322 This module provides common functionality used by the other modules in
323 this package.
326 # Whether or not to show debug messages
327 DEBUG = False
329 def notify(msg, *args):
330 """Print a message to stderr."""
331 print >> sys.stderr, msg % args
333 def debug (msg, *args):
334 """Print a debug message to stderr when DEBUG is enabled."""
335 if DEBUG:
336 print >> sys.stderr, msg % args
338 def error (msg, *args):
339 """Print an error message to stderr."""
340 print >> sys.stderr, "ERROR:", msg % args
342 def warn(msg, *args):
343 """Print a warning message to stderr."""
344 print >> sys.stderr, "warning:", msg % args
346 def die (msg, *args):
347 """Print as error message to stderr and exit the program."""
348 error(msg, *args)
349 sys.exit(1)
352 class ProgressIndicator(object):
354 """Simple progress indicator.
356 Displayed as a spinning character by default, but can be customized
357 by passing custom messages that overrides the spinning character.
361 States = ("|", "/", "-", "\\")
363 def __init__ (self, prefix = "", f = sys.stdout):
364 """Create a new ProgressIndicator, bound to the given file object."""
365 self.n = 0 # Simple progress counter
366 self.f = f # Progress is written to this file object
367 self.prev_len = 0 # Length of previous msg (to be overwritten)
368 self.prefix = prefix # Prefix prepended to each progress message
369 self.prefix_lens = [] # Stack of prefix string lengths
371 def pushprefix (self, prefix):
372 """Append the given prefix onto the prefix stack."""
373 self.prefix_lens.append(len(self.prefix))
374 self.prefix += prefix
376 def popprefix (self):
377 """Remove the last prefix from the prefix stack."""
378 prev_len = self.prefix_lens.pop()
379 self.prefix = self.prefix[:prev_len]
381 def __call__ (self, msg = None, lf = False):
382 """Indicate progress, possibly with a custom message."""
383 if msg is None:
384 msg = self.States[self.n % len(self.States)]
385 msg = self.prefix + msg
386 print >> self.f, "\r%-*s" % (self.prev_len, msg),
387 self.prev_len = len(msg.expandtabs())
388 if lf:
389 print >> self.f
390 self.prev_len = 0
391 self.n += 1
393 def finish (self, msg = "done", noprefix = False):
394 """Finalize progress indication with the given message."""
395 if noprefix:
396 self.prefix = ""
397 self(msg, True)
400 def start_command (args, cwd = None, shell = False, add_env = None,
401 stdin = subprocess.PIPE, stdout = subprocess.PIPE,
402 stderr = subprocess.PIPE):
403 """Start the given command, and return a subprocess object.
405 This provides a simpler interface to the subprocess module.
408 env = None
409 if add_env is not None:
410 env = os.environ.copy()
411 env.update(add_env)
412 return subprocess.Popen(args, bufsize = 1, stdin = stdin, stdout = stdout,
413 stderr = stderr, cwd = cwd, shell = shell,
414 env = env, universal_newlines = True)
417 def run_command (args, cwd = None, shell = False, add_env = None,
418 flag_error = True):
419 """Run the given command to completion, and return its results.
421 This provides a simpler interface to the subprocess module.
423 The results are formatted as a 3-tuple: (exit_code, output, errors)
425 If flag_error is enabled, Error messages will be produced if the
426 subprocess terminated with a non-zero exit code and/or stderr
427 output.
429 The other arguments are passed on to start_command().
432 process = start_command(args, cwd, shell, add_env)
433 (output, errors) = process.communicate()
434 exit_code = process.returncode
435 if flag_error and errors:
436 error("'%s' returned errors:\n---\n%s---", " ".join(args), errors)
437 if flag_error and exit_code:
438 error("'%s' returned exit code %i", " ".join(args), exit_code)
439 return (exit_code, output, errors)