2 # Copyright (c) 2008 David Aguilar
9 from cStringIO
import StringIO
11 from cola
import defaults
12 from cola
.exception
import ColaException
14 PREFIX
= os
.path
.realpath(os
.path
.dirname(os
.path
.dirname(sys
.argv
[0])))
15 QMDIR
= os
.path
.join(PREFIX
, 'share', 'cola', 'qm')
16 ICONSDIR
= os
.path
.join(PREFIX
, 'share', 'cola', 'icons')
17 STYLEDIR
= os
.path
.join(PREFIX
, 'share', 'cola', 'styles')
18 DOCDIR
= os
.path
.join(PREFIX
, 'share', 'doc', 'cola')
22 'python': 'script.png',
24 'shell': 'script.png',
27 'assembler': 'binary.png',
28 'binary': 'binary.png',
33 class RunCommandException(ColaException
):
34 """Thrown when something bad happened when we tried to run the
38 def run_cmd(*command
):
40 Runs a *command argument list and returns the output.
41 e.g. run_cmd("echo", "hello", "world")
45 proc
= subprocess
.Popen(command
,
46 stdout
=subprocess
.PIPE
,
47 stderr
=subprocess
.PIPE
)
49 # Wait for the process to return
50 stdout_value
= proc
.stdout
.read()
55 # Strip off trailing whitespace by default
56 return stdout_value
.rstrip()
58 raise RunCommandException('ERROR Running: [%s]' % ' '.join(command
))
61 def get_qm_for_locale(locale
):
62 regex
= re
.compile(r
'([^\.])+\..*$')
63 match
= regex
.match(locale
)
65 locale
= match
.group(1)
67 basename
= locale
.split('_')[0]
69 return os
.path
.join(QMDIR
, basename
+'.qm')
71 def get_resource_dirs(styledir
):
72 return [ r
for r
in glob(styledir
+ '/*') if os
.path
.isdir(r
) ]
74 def get_stylesheet(name
):
75 stylesheet
= os
.path
.join(STYLEDIR
, name
+'.qss')
76 if os
.path
.exists(stylesheet
):
82 return os
.path
.join(DOCDIR
, 'git-cola.html')
84 def ident_file_type(filename
):
85 """Returns an icon based on the contents of filename."""
86 if os
.path
.exists(filename
):
87 fileinfo
= run_cmd('file','-b',filename
)
88 for filetype
, iconname
in KNOWN_FILE_TYPES
.iteritems():
89 if filetype
in fileinfo
.lower():
93 # Fallback for modified files of an unknown type
96 def get_file_icon(filename
):
98 Returns the full path to an icon file corresponding to
101 icon_file
= ident_file_type(filename
)
102 return get_icon(icon_file
)
104 def get_icon(icon_file
):
105 return os
.path
.join(ICONSDIR
, icon_file
)
108 if os
.name
in ('nt', 'dos'):
109 for path
in os
.environ
['PATH'].split(os
.pathsep
):
110 file = os
.path
.join(path
, args
[0]) + ".exe"
112 return os
.spawnv(os
.P_NOWAIT
, file, (file,) + args
[1:])
115 raise IOError('cannot find executable: %s' % program
)
117 argv
= map(shell_quote
, args
)
118 return os
.system(' '.join(argv
) + '&')
129 def grep(pattern
, items
, squash
=True):
130 isdict
= type(items
) is dict
131 if pattern
in __grep_cache
:
132 regex
= __grep_cache
[pattern
]
134 regex
= __grep_cache
[pattern
] = re
.compile(pattern
)
138 match
= regex
.match(item
)
139 if not match
: continue
140 groups
= match
.groups()
142 subitems
= match
.group(0)
147 subitems
= list(groups
)
149 matchdict
[item
] = items
[item
]
151 matched
.append(subitems
)
156 if squash
and len(matched
) == 1:
162 """Avoid os.path.basename because we are explicitly
163 parsing git"s output, which contains /"s regardless
166 base_regex
= re
.compile('(.*?/)?([^/]+)$')
167 match
= base_regex
.match(path
)
169 return match
.group(2)
173 def shell_quote(*inputs
):
175 Quote strings so that they can be suitably martialled
176 off to the shell. This method supports POSIX sh syntax.
177 This is crucial to properly handle command line arguments
178 with spaces, quotes, double-quotes, etc.
181 regex
= re
.compile('[^\w!%+,\-./:@^]')
182 quote_regex
= re
.compile("((?:'\\''){2,})")
190 raise AssertionError,('No way to quote strings '
191 'containing null(\\000) bytes')
193 # = does need quoting else in command position it's a
194 # program-local environment setting
195 match
= regex
.search(input)
196 if match
and '=' not in input:
198 input = input.replace("'", "'\\''")
200 # make multiple ' in a row look simpler
201 # '\'''\'''\'' -> '"'''"'
202 quote_match
= quote_regex
.match(input)
204 quotes
= match
.group(1)
205 input.replace(quotes
, ("'" *(len(quotes
)/4)) + "\"'")
207 input = "'%s'" % input
208 if input.startswith("''"):
211 if input.endswith("''"):
218 pad
= HEADER_LENGTH
- len(msg
) - 4 # len(':+') + len('+:')
224 +(' ' * (pad
+ extra
))
228 def parse_geom(geomstr
):
229 regex
= re
.compile('^(\d+)x(\d+)\+(\d+),(\d+).*?')
230 match
= regex
.match(geomstr
)
232 defaults
.WIDTH
= int(match
.group(1))
233 defaults
.HEIGHT
= int(match
.group(2))
234 defaults
.X
= int(match
.group(3))
235 defaults
.Y
= int(match
.group(4))
236 return (defaults
.WIDTH
, defaults
.HEIGHT
, defaults
.X
, defaults
.Y
)
239 return ('%dx%d+%d,%d'
240 % (defaults
.WIDTH
, defaults
.HEIGHT
, defaults
.X
, defaults
.Y
))
243 return os
.path
.basename(defaults
.DIRECTORY
)
249 return slushy
.decode('utf-8')
251 def write(path
, contents
):
252 file = open(path
, 'w')
253 file.write(contents
.encode('utf-8'))
256 class DiffParser(object):
257 def __init__(self
, model
, filename
='',
258 cached
=True, branch
=None, reverse
=False):
260 self
.__header
_re
= re
.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
265 self
.__diff
_spans
= []
266 self
.__diff
_offsets
= []
274 (header
, diff
) = model
.diff_helper(filename
=filename
,
276 with_diff_header
=True,
277 cached
=cached
and not bool(branch
),
278 reverse
=cached
or bool(branch
) or reverse
)
282 self
.parse_diff(diff
)
284 # Always index into the non-reversed diff
285 self
.fwd_header
, self
.fwd_diff
= \
286 model
.diff_helper(filename
=filename
,
288 with_diff_header
=True,
289 cached
=cached
and not bool(branch
),
290 reverse
=bool(branch
))
292 def write_diff(self
,filename
,which
,selected
=False,noop
=False):
293 if not noop
and which
< len(self
.diffs
):
294 diff
= self
.diffs
[which
]
295 write(filename
, self
.header
+ '\n' + diff
+ '\n')
303 def get_diff_subset(self
, diff
, start
, end
):
308 offset
= self
.__diff
_spans
[diff
][0]
309 diffguts
= '\n'.join(self
.__diffs
[diff
])
311 for line
in self
.__diffs
[diff
]:
312 line_start
= offset
+ local_offset
313 local_offset
+= len(line
) + 1 #\n
314 line_end
= offset
+ local_offset
315 # |line1 |line2 |line3 |
318 # selection has head of diff (line3)
319 if start
< line_start
and end
> line_start
and end
< line_end
:
321 if line
.startswith('+'):
323 if line
.startswith('-'):
325 # selection has all of diff (line2)
326 elif start
<= line_start
and end
>= line_end
:
328 if line
.startswith('+'):
330 if line
.startswith('-'):
332 # selection has tail of diff (line1)
333 elif start
>= line_start
and start
< line_end
- 1:
335 if line
.startswith('+'):
337 if line
.startswith('-'):
340 # Don't add new lines unless selected
341 if line
.startswith('+'):
343 elif line
.startswith('-'):
344 # Don't remove lines unless selected
345 newdiff
.append(' ' + line
[1:])
349 new_count
= self
.__headers
[diff
][1] + adds
- deletes
350 if new_count
!= self
.__headers
[diff
][3]:
351 header
= '@@ -%d,%d +%d,%d @@' % (
352 self
.__headers
[diff
][0],
353 self
.__headers
[diff
][1],
354 self
.__headers
[diff
][2],
358 return (self
.header
+ '\n' + '\n'.join(newdiff
) + '\n')
361 return self
.__diff
_spans
363 def get_offsets(self
):
364 return self
.__diff
_offsets
366 def set_diff_to_offset(self
, offset
):
368 self
.diffs
, self
.selected
= self
.get_diff_for_offset(offset
)
370 def set_diffs_to_range(self
, start
, end
):
373 self
.diffs
, self
.selected
= self
.get_diffs_for_range(start
,end
)
375 def get_diff_for_offset(self
, offset
):
376 for idx
, diff_offset
in enumerate(self
.__diff
_offsets
):
377 if offset
< diff_offset
:
378 return (['\n'.join(self
.__diffs
[idx
])], [idx
])
381 def get_diffs_for_range(self
, start
, end
):
384 for idx
, span
in enumerate(self
.__diff
_spans
):
385 has_end_of_diff
= start
>= span
[0] and start
< span
[1]
386 has_all_of_diff
= start
<= span
[0] and end
>= span
[1]
387 has_head_of_diff
= end
>= span
[0] and end
<= span
[1]
389 selected_diff
=(has_end_of_diff
393 diff
= '\n'.join(self
.__diffs
[idx
])
396 return diffs
, indices
398 def parse_diff(self
, diff
):
403 for idx
, line
in enumerate(diff
.splitlines()):
404 match
= self
.__header
_re
.match(line
)
406 self
.__headers
.append([
412 self
.__diffs
.append( [line
] )
414 line_len
= len(line
) + 1 #\n
415 self
.__diff
_spans
.append([total_offset
,
416 total_offset
+ line_len
])
417 total_offset
+= line_len
418 self
.__diff
_offsets
.append(total_offset
)
422 errmsg
= 'Malformed diff?\n\n%s' % diff
423 raise AssertionError, errmsg
424 line_len
= len(line
) + 1
425 total_offset
+= line_len
427 self
.__diffs
[self
.__idx
].append(line
)
428 self
.__diff
_spans
[-1][-1] += line_len
429 self
.__diff
_offsets
[self
.__idx
] += line_len
431 def process_diff_selection(self
, selected
, offset
, selection
,
432 apply_to_worktree
=False):
434 start
= self
.fwd_diff
.index(selection
)
435 end
= start
+ len(selection
)
436 self
.set_diffs_to_range(start
, end
)
438 self
.set_diff_to_offset(offset
)
440 # Process diff selection only
442 for idx
in self
.selected
:
443 contents
= self
.get_diff_subset(idx
, start
, end
)
445 tmpfile
= self
.model
.get_tmp_filename()
446 write(tmpfile
, contents
)
447 if apply_to_worktree
:
448 self
.model
.apply_diff_to_worktree(tmpfile
)
450 self
.model
.apply_diff(tmpfile
)
452 # Process a complete hunk
454 for idx
, diff
in enumerate(self
.diffs
):
455 tmpfile
= self
.model
.get_tmp_filename()
456 if self
.write_diff(tmpfile
,idx
):
457 if apply_to_worktree
:
458 self
.model
.apply_diff_to_worktree(tmpfile
)
460 self
.model
.apply_diff(tmpfile
)
463 def strip_prefix(prefix
, string
):
464 """Return string, without the prefix. Blow up if string doesn't
465 start with prefix."""
466 assert string
.startswith(prefix
)
467 return string
[len(prefix
):]
469 def sanitize_input(input):
470 for c
in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
471 input = input.replace(c
, '_')
475 return platform
.system() == 'Linux'
478 return os
.path
.exists('/usr/bin/apt-get')
481 return (platform
.system() == 'Windows'
482 or 'Macintosh' in platform
.platform())