2 # Copyright (c) 2008 David Aguilar
3 """This module provides miscellaneous utility functions."""
11 from cStringIO
import StringIO
13 from cola
import defaults
15 from cola
.git
import shell_quote
16 from cola
.core
import encode
, decode
17 from cola
.exception
import ColaException
19 PREFIX
= os
.path
.realpath(os
.path
.dirname(os
.path
.dirname(sys
.argv
[0])))
20 QMDIR
= os
.path
.join(PREFIX
, 'share', 'cola', 'qm')
21 ICONSDIR
= os
.path
.join(PREFIX
, 'share', 'cola', 'icons')
22 STYLEDIR
= os
.path
.join(PREFIX
, 'share', 'cola', 'styles')
23 DOCDIR
= os
.path
.join(PREFIX
, 'share', 'doc', 'cola')
27 'python': 'script.png',
29 'shell': 'script.png',
32 'assembler': 'binary.png',
33 'binary': 'binary.png',
38 def run_cmd(*command
):
40 Runs a *command argument list and returns the output.
41 e.g. run_cmd("echo", "hello", "world")
43 return git
.Git
.execute(command
)
45 def get_qm_for_locale(locale
):
46 """Returns the .qm file for a particular $LANG values."""
47 regex
= re
.compile(r
'([^\.])+\..*$')
48 match
= regex
.match(locale
)
50 locale
= match
.group(1)
52 basename
= locale
.split('_')[0]
54 return os
.path
.join(QMDIR
, basename
+'.qm')
56 def get_resource_dirs(styledir
):
57 """Returns all directories underneath the share/cola/styles directory."""
58 return [ r
for r
in glob(styledir
+ '/*') if os
.path
.isdir(r
) ]
60 def get_stylesheet(name
):
61 """Returns the path to a stylesheet within the cola install tree."""
62 stylesheet
= os
.path
.join(STYLEDIR
, name
+'.qss')
63 if os
.path
.exists(stylesheet
):
69 """Returns the path to the cola html documentation."""
70 return os
.path
.join(DOCDIR
, 'git-cola.html')
72 def ident_file_type(filename
):
73 """Returns an icon based on the contents of filename."""
74 if os
.path
.exists(filename
):
75 fileinfo
= run_cmd('file','-b',filename
)
76 for filetype
, iconname
in KNOWN_FILE_TYPES
.iteritems():
77 if filetype
in fileinfo
.lower():
81 # Fallback for modified files of an unknown type
84 def get_file_icon(filename
):
86 Returns the full path to an icon file corresponding to
89 icon_file
= ident_file_type(filename
)
90 return get_icon(icon_file
)
92 def get_icon(icon_file
):
93 """Returns the full path to an icon file given a basename."""
94 return os
.path
.join(ICONSDIR
, icon_file
)
97 """Launches a command in the background."""
98 args
= tuple([ encode(a
) for a
in args
])
99 if os
.name
in ('nt', 'dos'):
100 for path
in os
.environ
['PATH'].split(os
.pathsep
):
101 filename
= os
.path
.join(path
, args
[0]) + ".exe"
102 if os
.path
.exists(filename
):
104 return os
.spawnv(os
.P_NOWAIT
, filename
, args
)
107 raise IOError('cannot find executable: %s' % program
)
109 argv
= map(shell_quote
, args
)
110 return os
.system(' '.join(argv
) + '&')
113 """Subtracts list b from list a and returns the resulting list."""
114 # conceptually, c = a - b
122 def grep(pattern
, items
, squash
=True):
123 """Greps a list for items that match a pattern and return a list of
124 matching items. If only one item matches, return just that item.
126 isdict
= type(items
) is dict
127 if pattern
in __grep_cache
:
128 regex
= __grep_cache
[pattern
]
130 regex
= __grep_cache
[pattern
] = re
.compile(pattern
)
134 match
= regex
.match(item
)
135 if not match
: continue
136 groups
= match
.groups()
138 subitems
= match
.group(0)
143 subitems
= list(groups
)
145 matchdict
[item
] = items
[item
]
147 matched
.append(subitems
)
152 if squash
and len(matched
) == 1:
158 """Avoid os.path.basename because we are explicitly
159 parsing git"s output, which contains /"s regardless
162 base_regex
= re
.compile('(.*?/)?([^/]+)$')
163 match
= base_regex
.match(path
)
165 return match
.group(2)
171 pad
= HEADER_LENGTH
- len(msg
) - 4 # len(':+') + len('+:')
177 +(' ' * (pad
+ extra
))
181 def parse_geom(geomstr
):
182 regex
= re
.compile('^(\d+)x(\d+)\+(\d+),(\d+).*?')
183 match
= regex
.match(geomstr
)
185 defaults
.WIDTH
= int(match
.group(1))
186 defaults
.HEIGHT
= int(match
.group(2))
187 defaults
.X
= int(match
.group(3))
188 defaults
.Y
= int(match
.group(4))
189 return (defaults
.WIDTH
, defaults
.HEIGHT
, defaults
.X
, defaults
.Y
)
192 return ('%dx%d+%d,%d'
193 % (defaults
.WIDTH
, defaults
.HEIGHT
, defaults
.X
, defaults
.Y
))
196 return os
.path
.basename(defaults
.DIRECTORY
)
199 """Slurps a filepath into a string."""
203 return decode(slushy
)
205 def write(path
, contents
):
206 """Writes a string to a file."""
207 file = open(path
, 'w')
208 file.write(encode(contents
))
211 class DiffParser(object):
212 """Handles parsing diff for use by the interactive index editor."""
213 def __init__(self
, model
, filename
='',
214 cached
=True, branch
=None, reverse
=False):
216 self
.__header
_re
= re
.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
221 self
.__diff
_spans
= []
222 self
.__diff
_offsets
= []
230 (header
, diff
) = model
.diff_helper(filename
=filename
,
232 with_diff_header
=True,
233 cached
=cached
and not bool(branch
),
234 reverse
=cached
or bool(branch
) or reverse
)
238 self
.parse_diff(diff
)
240 # Always index into the non-reversed diff
241 self
.fwd_header
, self
.fwd_diff
= \
242 model
.diff_helper(filename
=filename
,
244 with_diff_header
=True,
245 cached
=cached
and not bool(branch
),
246 reverse
=bool(branch
))
248 def write_diff(self
,filename
,which
,selected
=False,noop
=False):
249 """Writes a new diff corresponding to the user's selection."""
250 if not noop
and which
< len(self
.diffs
):
251 diff
= self
.diffs
[which
]
252 write(filename
, self
.header
+ '\n' + diff
+ '\n')
258 """Returns the list of diffs."""
261 def get_diff_subset(self
, diff
, start
, end
):
262 """Processes the diffs and returns a selected subset from that diff.
268 offset
= self
.__diff
_spans
[diff
][0]
269 diffguts
= '\n'.join(self
.__diffs
[diff
])
271 for line
in self
.__diffs
[diff
]:
272 line_start
= offset
+ local_offset
273 local_offset
+= len(line
) + 1 #\n
274 line_end
= offset
+ local_offset
275 # |line1 |line2 |line3 |
278 # selection has head of diff (line3)
279 if start
< line_start
and end
> line_start
and end
< line_end
:
281 if line
.startswith('+'):
283 if line
.startswith('-'):
285 # selection has all of diff (line2)
286 elif start
<= line_start
and end
>= line_end
:
288 if line
.startswith('+'):
290 if line
.startswith('-'):
292 # selection has tail of diff (line1)
293 elif start
>= line_start
and start
< line_end
- 1:
295 if line
.startswith('+'):
297 if line
.startswith('-'):
300 # Don't add new lines unless selected
301 if line
.startswith('+'):
303 elif line
.startswith('-'):
304 # Don't remove lines unless selected
305 newdiff
.append(' ' + line
[1:])
309 new_count
= self
.__headers
[diff
][1] + adds
- deletes
310 if new_count
!= self
.__headers
[diff
][3]:
311 header
= '@@ -%d,%d +%d,%d @@' % (
312 self
.__headers
[diff
][0],
313 self
.__headers
[diff
][1],
314 self
.__headers
[diff
][2],
318 return (self
.header
+ '\n' + '\n'.join(newdiff
) + '\n')
321 """Returns the line spans of each hunk."""
322 return self
.__diff
_spans
324 def get_offsets(self
):
325 """Returns the offsets."""
326 return self
.__diff
_offsets
328 def set_diff_to_offset(self
, offset
):
329 """Sets the diff selection to be the hunk at a particular offset."""
331 self
.diffs
, self
.selected
= self
.get_diff_for_offset(offset
)
333 def set_diffs_to_range(self
, start
, end
):
334 """Sets the diff selection to be a range of hunks."""
337 self
.diffs
, self
.selected
= self
.get_diffs_for_range(start
,end
)
339 def get_diff_for_offset(self
, offset
):
340 """Returns the hunks for a particular offset."""
341 for idx
, diff_offset
in enumerate(self
.__diff
_offsets
):
342 if offset
< diff_offset
:
343 return (['\n'.join(self
.__diffs
[idx
])], [idx
])
346 def get_diffs_for_range(self
, start
, end
):
347 """Returns the hunks for a selected range."""
350 for idx
, span
in enumerate(self
.__diff
_spans
):
351 has_end_of_diff
= start
>= span
[0] and start
< span
[1]
352 has_all_of_diff
= start
<= span
[0] and end
>= span
[1]
353 has_head_of_diff
= end
>= span
[0] and end
<= span
[1]
355 selected_diff
=(has_end_of_diff
359 diff
= '\n'.join(self
.__diffs
[idx
])
362 return diffs
, indices
364 def parse_diff(self
, diff
):
365 """Parses a diff and extracts headers, offsets, hunks, etc.
371 for idx
, line
in enumerate(diff
.splitlines()):
372 match
= self
.__header
_re
.match(line
)
374 self
.__headers
.append([
380 self
.__diffs
.append( [line
] )
382 line_len
= len(line
) + 1 #\n
383 self
.__diff
_spans
.append([total_offset
,
384 total_offset
+ line_len
])
385 total_offset
+= line_len
386 self
.__diff
_offsets
.append(total_offset
)
390 errmsg
= 'Malformed diff?\n\n%s' % diff
391 raise AssertionError, errmsg
392 line_len
= len(line
) + 1
393 total_offset
+= line_len
395 self
.__diffs
[self
.__idx
].append(line
)
396 self
.__diff
_spans
[-1][-1] += line_len
397 self
.__diff
_offsets
[self
.__idx
] += line_len
399 def process_diff_selection(self
, selected
, offset
, selection
,
400 apply_to_worktree
=False):
401 """Processes a diff selection and applies changes to the work tree
404 start
= self
.fwd_diff
.index(selection
)
405 end
= start
+ len(selection
)
406 self
.set_diffs_to_range(start
, end
)
408 self
.set_diff_to_offset(offset
)
410 # Process diff selection only
412 for idx
in self
.selected
:
413 contents
= self
.get_diff_subset(idx
, start
, end
)
415 tmpfile
= self
.model
.get_tmp_filename()
416 write(tmpfile
, contents
)
417 if apply_to_worktree
:
418 self
.model
.apply_diff_to_worktree(tmpfile
)
420 self
.model
.apply_diff(tmpfile
)
422 # Process a complete hunk
424 for idx
, diff
in enumerate(self
.diffs
):
425 tmpfile
= self
.model
.get_tmp_filename()
426 if self
.write_diff(tmpfile
,idx
):
427 if apply_to_worktree
:
428 self
.model
.apply_diff_to_worktree(tmpfile
)
430 self
.model
.apply_diff(tmpfile
)
433 def strip_prefix(prefix
, string
):
434 """Return string, without the prefix. Blow up if string doesn't
435 start with prefix."""
436 assert string
.startswith(prefix
)
437 return string
[len(prefix
):]
439 def sanitize_input(input):
440 """Removes shell metacharacters from a string."""
441 for c
in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
442 input = input.replace(c
, '_')
446 """Is this a linux machine?"""
447 return platform
.system() == 'Linux'
451 return os
.path
.exists('/usr/bin/apt-get')
454 """Is it windows or mac? (e.g. is running git-mergetool non-trivial?)"""
455 return (platform
.system() == 'Windows'
456 or 'Macintosh' in platform
.platform())