xbase: allow "reword" and "edit" as first choices
[git-cola.git] / cola / utils.py
blob52775ab3fe2609620263e28db0d3714d5987f0bc
1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous utility functions."""
4 import mimetypes
5 import os
6 import random
7 import re
8 import shlex
9 import sys
10 import time
11 import traceback
13 from cola import core
14 from cola import resources
15 from cola.compat import hashlib
17 random.seed(hash(time.time()))
20 KNOWN_FILE_MIME_TYPES = {
21 'text': 'script.png',
22 'image': 'image.png',
23 'python': 'script.png',
24 'ruby': 'script.png',
25 'shell': 'script.png',
26 'perl': 'script.png',
27 'octet': 'binary.png',
30 KNOWN_FILE_EXTENSION = {
31 '.java': 'script.png',
32 '.groovy': 'script.png',
33 '.cpp': 'script.png',
34 '.c': 'script.png',
35 '.h': 'script.png',
36 '.cxx': 'script.png',
40 def add_parents(path_entry_set):
41 """Iterate over each item in the set and add its parent directories."""
42 for path in list(path_entry_set):
43 while '//' in path:
44 path = path.replace('//', '/')
45 if path not in path_entry_set:
46 path_entry_set.add(path)
47 if '/' in path:
48 parent_dir = dirname(path)
49 while parent_dir and parent_dir not in path_entry_set:
50 path_entry_set.add(parent_dir)
51 parent_dir = dirname(parent_dir)
52 return path_entry_set
55 def ident_file_type(filename):
56 """Returns an icon based on the contents of filename."""
57 if core.exists(filename):
58 filemimetype = mimetypes.guess_type(filename)
59 if filemimetype[0] != None:
60 for filetype, iconname in KNOWN_FILE_MIME_TYPES.iteritems():
61 if filetype in filemimetype[0].lower():
62 return iconname
63 filename = filename.lower()
64 for fileext, iconname in KNOWN_FILE_EXTENSION.iteritems():
65 if filename.endswith(fileext):
66 return iconname
67 return 'generic.png'
68 else:
69 return 'removed.png'
70 # Fallback for modified files of an unknown type
71 return 'generic.png'
74 def file_icon(filename):
75 """
76 Returns the full path to an icon file corresponding to
77 filename"s contents.
78 """
79 return resources.icon(ident_file_type(filename))
82 def format_exception(e):
83 exc_type, exc_value, exc_tb = sys.exc_info()
84 details = traceback.format_exception(exc_type, exc_value, exc_tb)
85 details = '\n'.join(details)
86 if hasattr(e, 'msg'):
87 msg = e.msg
88 else:
89 msg = str(e)
90 return (msg, details)
93 def sublist(a,b):
94 """Subtracts list b from list a and returns the resulting list."""
95 # conceptually, c = a - b
96 c = []
97 for item in a:
98 if item not in b:
99 c.append(item)
100 return c
103 __grep_cache = {}
104 def grep(pattern, items, squash=True):
105 """Greps a list for items that match a pattern and return a list of
106 matching items. If only one item matches, return just that item.
108 isdict = type(items) is dict
109 if pattern in __grep_cache:
110 regex = __grep_cache[pattern]
111 else:
112 regex = __grep_cache[pattern] = re.compile(pattern)
113 matched = []
114 matchdict = {}
115 for item in items:
116 match = regex.match(item)
117 if not match:
118 continue
119 groups = match.groups()
120 if not groups:
121 subitems = match.group(0)
122 else:
123 if len(groups) == 1:
124 subitems = groups[0]
125 else:
126 subitems = list(groups)
127 if isdict:
128 matchdict[item] = items[item]
129 else:
130 matched.append(subitems)
132 if isdict:
133 return matchdict
134 else:
135 if squash and len(matched) == 1:
136 return matched[0]
137 else:
138 return matched
141 def basename(path):
143 An os.path.basename() implementation that always uses '/'
145 Avoid os.path.basename because git's output always
146 uses '/' regardless of platform.
149 return path.rsplit('/', 1)[-1]
152 def strip_one(path):
153 """Strip one level of directory
155 >>> strip_one('/usr/bin/git')
156 'bin/git'
158 >>> strip_one('local/bin/git')
159 'bin/git'
161 >>> strip_one('bin/git')
162 'git'
164 >>> strip_one('git')
165 'git'
168 return path.strip('/').split('/', 1)[-1]
171 def dirname(path):
173 An os.path.dirname() implementation that always uses '/'
175 Avoid os.path.dirname because git's output always
176 uses '/' regardless of platform.
179 while '//' in path:
180 path = path.replace('//', '/')
181 path_dirname = path.rsplit('/', 1)[0]
182 if path_dirname == path:
183 return ''
184 return path.rsplit('/', 1)[0]
187 def strip_prefix(prefix, string):
188 """Return string, without the prefix. Blow up if string doesn't
189 start with prefix."""
190 assert string.startswith(prefix)
191 return string[len(prefix):]
194 def sanitize(s):
195 """Removes shell metacharacters from a string."""
196 for c in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
197 s = s.replace(c, '_')
198 return s
201 def tablength(word, tabwidth):
202 """Return length of a word taking tabs into account
204 >>> tablength("\\t\\t\\t\\tX", 8)
208 return len(word.replace('\t', '')) + word.count('\t') * tabwidth
211 def _shell_split(s):
212 """Split string apart into utf-8 encoded words using shell syntax"""
213 try:
214 return shlex.split(core.encode(s))
215 except ValueError:
216 return [core.encode(s)]
219 def shell_split(s):
220 """Returns a unicode list instead of encoded strings"""
221 return [core.decode(arg) for arg in _shell_split(s)]
224 def tmp_dir():
225 # Allow TMPDIR/TMP with a fallback to /tmp
226 return core.getenv('TMP', core.getenv('TMPDIR', '/tmp'))
229 def tmp_file_pattern():
230 return os.path.join(tmp_dir(), 'git-cola-%s-.*' % os.getpid())
233 def tmp_filename(prefix):
234 randstr = ''.join([chr(random.randint(ord('a'), ord('z')))
235 for i in range(7)])
236 prefix = prefix.replace('/', '-').replace('\\', '-')
237 basename = 'git-cola-%s-%s-%s' % (os.getpid(), randstr, prefix)
238 return os.path.join(tmp_dir(), basename)
241 def is_linux():
242 """Is this a linux machine?"""
243 return sys.platform.startswith('linux')
246 def is_debian():
247 """Is it debian?"""
248 return os.path.exists('/usr/bin/apt-get')
251 def is_darwin():
252 """Return True on OSX."""
253 return sys.platform == 'darwin'
256 def is_win32():
257 """Return True on win32"""
258 return sys.platform == 'win32' or sys.platform == 'cygwin'
261 def checksum(path):
262 """Return a cheap md5 hexdigest for a path."""
263 md5 = hashlib.new('md5')
264 md5.update(core.read(path))
265 return md5.hexdigest()
268 class ProgressIndicator(object):
270 """Simple progress indicator.
272 Displayed as a spinning character by default, but can be customized
273 by passing custom messages that overrides the spinning character.
277 States = ("|", "/", "-", "\\")
279 def __init__(self, prefix="", f=sys.stdout):
280 """Create a new ProgressIndicator, bound to the given file object."""
281 self.n = 0 # Simple progress counter
282 self.f = f # Progress is written to this file object
283 self.prev_len = 0 # Length of previous msg (to be overwritten)
284 self.prefix = prefix # Prefix prepended to each progress message
285 self.prefix_lens = [] # Stack of prefix string lengths
287 def pushprefix(self, prefix):
288 """Append the given prefix onto the prefix stack."""
289 self.prefix_lens.append(len(self.prefix))
290 self.prefix += prefix
292 def popprefix(self):
293 """Remove the last prefix from the prefix stack."""
294 prev_len = self.prefix_lens.pop()
295 self.prefix = self.prefix[:prev_len]
297 def __call__(self, msg = None, lf=False):
298 """Indicate progress, possibly with a custom message."""
299 if msg is None:
300 msg = self.States[self.n % len(self.States)]
301 msg = self.prefix + msg
302 print >> self.f, "\r%-*s" % (self.prev_len, msg),
303 self.prev_len = len(msg.expandtabs())
304 if lf:
305 print >> self.f
306 self.prev_len = 0
307 self.n += 1
309 def finish (self, msg="done", noprefix=False):
310 """Finalize progress indication with the given message."""
311 if noprefix:
312 self.prefix = ""
313 self(msg, True)