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
.git
import shell_quote
21 KNOWN_FILE_MIME_TYPES
= {
24 'python': 'script.png',
26 'shell': 'script.png',
28 'octet': 'binary.png',
31 KNOWN_FILE_EXTENSION
= {
32 '.java': 'script.png',
33 '.groovy': '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
):
45 path
= path
.replace('//', '/')
46 if path
not in path_entry_set
:
47 path_entry_set
.add(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
)
58 Run arguments as a command and return output.
60 >>> run_cmd(["echo", "hello", "world"])
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():
75 filename
= filename
.lower()
76 for fileext
, iconname
in KNOWN_FILE_EXTENSION
.iteritems():
77 if filename
.endswith(fileext
):
82 # Fallback for modified files of an unknown type
86 def file_icon(filename
):
88 Returns the full path to an icon file corresponding to
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
):
98 if not exe
.endswith('.exe'):
100 if os
.path
.exists(exe
):
102 for path
in os
.environ
['PATH'].split(os
.pathsep
):
103 abspath
= os
.path
.join(path
, exe
)
104 if os
.path
.exists(abspath
):
109 def win32_expand_paths(args
):
110 """Expand filenames after the double-dash"""
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
)))
121 """Launch a command in the background."""
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])
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
)
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
+ '&')
153 """Subtracts list b from list a and returns the resulting list."""
154 # conceptually, c = a - b
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
]
171 regex
= __grep_cache
[pattern
] = re
.compile(pattern
)
175 match
= regex
.match(item
)
178 groups
= match
.groups()
180 subitems
= match
.group(0)
185 subitems
= list(groups
)
187 matchdict
[item
] = items
[item
]
189 matched
.append(subitems
)
194 if squash
and len(matched
) == 1:
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]
213 An os.path.dirname() implementation that always uses '/'
215 Avoid os.path.dirname because git's output always
216 uses '/' regardless of platform.
220 path
= path
.replace('//', '/')
221 path_dirname
= path
.rsplit('/', 1)[0]
222 if path_dirname
== path
:
224 return path
.rsplit('/', 1)[0]
228 """Slurps a filepath into a string."""
230 slushy
= core
.read_nointr(fh
)
232 return core
.decode(slushy
)
235 def write(path
, contents
):
236 """Writes a string to a file."""
238 core
.write_nointr(fh
, core
.encode(contents
))
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
):]
248 """Removes shell metacharacters from a string."""
249 for c
in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
250 s
= s
.replace(c
, '_')
254 """Is this a linux machine?"""
257 return platform
.system() == 'Linux'
259 if e
.errno
== errno
.EINTR
:
265 return os
.path
.exists('/usr/bin/apt-get')
269 """Return True on OSX."""
272 p
= platform
.platform()
275 if e
.errno
== errno
.EINTR
:
279 return 'macintosh' in p
or 'darwin' in p
284 """Return True on win32"""
286 if _is_win32
is None:
287 _is_win32
= os
.name
in ('nt', 'dos')
292 """Is it windows or mac? (e.g. is running git-mergetool non-trivial?)"""
297 return platform
.system() == 'Windows'
299 if e
.errno
== errno
.EINTR
:
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."""
314 repopath
= '"%s"' % repopath
318 """Misc. useful functionality used by the rest of this package.
320 This module provides common functionality used by the other modules in
324 # Whether or not to show debug messages
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."""
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."""
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."""
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())
391 def finish (self
, msg
= "done", noprefix
= False):
392 """Finalize progress indication with the given message."""
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.
407 if add_env
is not None:
408 env
= os
.environ
.copy()
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,
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
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
)