1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous utility functions."""
14 from cStringIO
import StringIO
18 from cola
import resources
19 from cola
.compat
import hashlib
20 from cola
.decorators
import memoize
23 KNOWN_FILE_MIME_TYPES
= {
26 'python': 'script.png',
28 'shell': 'script.png',
30 'octet': 'binary.png',
33 KNOWN_FILE_EXTENSION
= {
34 '.java': 'script.png',
35 '.groovy': '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
):
47 path
= path
.replace('//', '/')
48 if path
not in path_entry_set
:
49 path_entry_set
.add(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
)
60 Run arguments as a command and return output.
62 >>> run_cmd(["echo", "hello", "world"])
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():
77 filename
= filename
.lower()
78 for fileext
, iconname
in KNOWN_FILE_EXTENSION
.iteritems():
79 if filename
.endswith(fileext
):
84 # Fallback for modified files of an unknown type
88 def file_icon(filename
):
90 Returns the full path to an icon file corresponding to
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
):
100 if not exe
.endswith('.exe'):
102 if os
.path
.exists(exe
):
104 for path
in os
.environ
['PATH'].split(os
.pathsep
):
105 abspath
= os
.path
.join(path
, exe
)
106 if os
.path
.exists(abspath
):
111 def win32_expand_paths(args
):
112 """Expand and quote filenames after the double-dash"""
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
)))
123 """Launch a command in the background."""
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])
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
)
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
+ '&')
155 """Subtracts list b from list a and returns the resulting list."""
156 # conceptually, c = a - b
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
]
173 regex
= __grep_cache
[pattern
] = re
.compile(pattern
)
177 match
= regex
.match(item
)
180 groups
= match
.groups()
182 subitems
= match
.group(0)
187 subitems
= list(groups
)
189 matchdict
[item
] = items
[item
]
191 matched
.append(subitems
)
196 if squash
and len(matched
) == 1:
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]
215 An os.path.dirname() implementation that always uses '/'
217 Avoid os.path.dirname because git's output always
218 uses '/' regardless of platform.
222 path
= path
.replace('//', '/')
223 path_dirname
= path
.rsplit('/', 1)[0]
224 if path_dirname
== path
:
226 return path
.rsplit('/', 1)[0]
230 """Slurps a filepath into a string."""
232 slushy
= core
.read_nointr(fh
)
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
))
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
):]
251 """Removes shell metacharacters from a string."""
252 for c
in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
253 s
= s
.replace(c
, '_')
257 """Is this a linux machine?"""
260 return platform
.system() == 'Linux'
262 if e
.errno
== errno
.EINTR
:
268 return os
.path
.exists('/usr/bin/apt-get')
272 """Return True on OSX."""
275 p
= platform
.platform()
278 if e
.errno
== errno
.EINTR
:
282 return 'macintosh' in p
or 'darwin' in p
287 """Return True on win32"""
288 return os
.name
in ('nt', 'dos')
293 """Is it windows or mac? (e.g. is running git-mergetool non-trivial?)"""
298 return platform
.system() == 'Windows'
300 if e
.errno
== errno
.EINTR
:
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."""
316 repopath
= '"%s"' % repopath
320 """Misc. useful functionality used by the rest of this package.
322 This module provides common functionality used by the other modules in
326 # Whether or not to show debug messages
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."""
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."""
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."""
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())
393 def finish (self
, msg
= "done", noprefix
= False):
394 """Finalize progress indication with the given message."""
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.
409 if add_env
is not None:
410 env
= os
.environ
.copy()
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,
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
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
)