6 from cStringIO
import StringIO
7 from PyQt4
.QtCore
import QProcess
11 PREFIX
= os
.path
.realpath(os
.path
.dirname(os
.path
.dirname(sys
.argv
[0])))
12 QMDIR
= os
.path
.join(PREFIX
, 'share', 'ugit', 'qm')
13 ICONSDIR
= os
.path
.join(PREFIX
, 'share', 'ugit', 'icons')
16 'python': 'script.png',
18 'shell': 'script.png',
21 'assembler': 'binary.png',
22 'binary': 'binary.png',
27 def get_qm_for_locale(locale
):
28 regex
= re
.compile(r
'([^\.])+\..*$')
29 match
= regex
.match(locale
)
31 locale
= match
.group(1)
33 basename
= locale
.split('_')[0]
35 return os
.path
.join(QMDIR
, basename
+'.qm')
37 def ident_file_type(filename
):
38 '''Returns an icon based on the contents of filename.'''
39 if os
.path
.exists(filename
):
40 fileinfo
= run_cmd('file','-b',filename
)
41 for filetype
, iconname
in KNOWN_FILE_TYPES
.iteritems():
42 if filetype
in fileinfo
.lower():
46 # Fallback for modified files of an unknown type
49 def get_file_icon(filename
):
50 '''Returns the full path to an icon file corresponding to
51 filename's contents.'''
52 icon_file
= ident_file_type(filename
)
53 return get_icon(icon_file
)
55 def get_icon(icon_file
):
56 return os
.path
.join(ICONSDIR
, icon_file
)
64 def run_cmd(cmd
, *args
, **kwargs
):
66 Returns an array of strings from the command's output.
68 raw: off -> passing raw=True returns a string instead of a list of strings.
69 with_status: off -> passing with_status=True returns
70 tuple(status,output) instead of just output
72 run_command(foo fi, bar, buzz, baz=value, q=None)
74 argv=[foo fi -q --baz=value bar buzz]
78 raw
= pop_key(kwargs
, 'raw')
79 with_status
= pop_key(kwargs
,'with_status')
80 with_stderr
= not pop_key(kwargs
,'without_stderr')
82 for k
,v
in kwargs
.iteritems():
85 kwarglist
.append("--%s" % k
)
86 elif v
is not None and type(v
) is not bool:
87 kwarglist
.append("--%s=%s" % (k
,v
))
90 kwarglist
.append("-%s" % k
)
91 elif v
is not None and type(v
) is not bool:
92 kwarglist
.append("-%s" % k
)
93 kwarglist
.append(str(v
))
94 # Handle cmd as either a string or an argv list
96 # we only call run_cmd(str) with str='git command'
97 # or other simple commands
102 cmd
= list(cmd
+ kwarglist
+ list(args
))
106 child
.setProcessChannelMode(QProcess
.MergedChannels
);
108 child
.start(cmd
[0], cmd
[1:])
109 if not (child
.waitForStarted() and child
.waitForFinished()):
111 output
= str(child
.readAll())
112 # run_cmd('cmd', *args, raw=True) if we want the full, raw output
115 return (child
.exitCode(), output
)
119 # simplify parsing by removing trailing \n from commands
121 return child
.exitCode(), output
[:-1]
139 def grep(pattern
, items
, squash
=True):
140 isdict
= type(items
) is dict
141 if pattern
in __grep_cache
:
142 regex
= __grep_cache
[pattern
]
144 regex
= __grep_cache
[pattern
] = re
.compile(pattern
)
148 match
= regex
.match(item
)
149 if not match
: continue
150 groups
= match
.groups()
152 subitems
= match
.group(0)
157 subitems
= list(groups
)
159 matchdict
[item
] = items
[item
]
161 matched
.append(subitems
)
166 if squash
and len(matched
) == 1:
172 '''Avoid os.path.basename because we are explicitly
173 parsing git's output, which contains /'s regardless
176 base_regex
= re
.compile('(.*?/)?([^/]+)$')
177 match
= base_regex
.match(path
)
179 return match
.group(2)
183 def shell_quote(*inputs
):
184 '''Quote strings so that they can be suitably martialled
185 off to the shell. This method supports POSIX sh syntax.
186 This is crucial to properly handle command line arguments
187 with spaces, quotes, double-quotes, etc.'''
189 regex
= re
.compile('[^\w!%+,\-./:@^]')
190 quote_regex
= re
.compile("((?:'\\''){2,})")
198 raise AssertionError,('No way to quote strings '
199 'containing null(\\000) bytes')
201 # = does need quoting else in command position it's a
202 # program-local environment setting
203 match
= regex
.search(input)
204 if match
and '=' not in input:
206 input = input.replace("'", "'\\''")
208 # make multiple ' in a row look simpler
209 # '\'''\'''\'' -> '"'''"'
210 quote_match
= quote_regex
.match(input)
212 quotes
= match
.group(1)
213 input.replace(quotes
,
214 ("'" *(len(quotes
)/4)) + "\"'")
216 input = "'%s'" % input
217 if input.startswith("''"):
220 if input.endswith("''"):
225 def get_tmp_filename():
226 # Allow TMPDIR/TMP with a fallback to /tmp
227 return '.ugit.%s.%s' %( os
.getpid(), time
.time() )
231 pad
= HEADER_LENGTH
- len(msg
) - 4 # len(':+') + len('+:')
237 +(' ' *(pad
+ extra
))
241 def parse_geom(geomstr
):
242 regex
= re
.compile('^(\d+)x(\d+)\+(\d+),(\d+) (\d+),(\d+) (\d+),(\d+)')
243 match
= regex
.match(geomstr
)
245 defaults
.WIDTH
= int(match
.group(1))
246 defaults
.HEIGHT
= int(match
.group(2))
247 defaults
.X
= int(match
.group(3))
248 defaults
.Y
= int(match
.group(4))
249 defaults
.SPLITTER_TOP_0
= int(match
.group(5))
250 defaults
.SPLITTER_TOP_1
= int(match
.group(6))
251 defaults
.SPLITTER_BOTTOM_0
= int(match
.group(7))
252 defaults
.SPLITTER_BOTTOM_1
= int(match
.group(8))
254 return (defaults
.WIDTH
, defaults
.HEIGHT
,
255 defaults
.X
, defaults
.Y
,
256 defaults
.SPLITTER_TOP_0
, defaults
.SPLITTER_TOP_1
,
257 defaults
.SPLITTER_BOTTOM_0
, defaults
.SPLITTER_BOTTOM_1
)
260 return '%dx%d+%d,%d %d,%d %d,%d' % (
261 defaults
.WIDTH
, defaults
.HEIGHT
,
262 defaults
.X
, defaults
.Y
,
263 defaults
.SPLITTER_TOP_0
, defaults
.SPLITTER_TOP_1
,
264 defaults
.SPLITTER_BOTTOM_0
, defaults
.SPLITTER_BOTTOM_1
)
267 return os
.path
.basename(defaults
.DIRECTORY
)
275 def write(path
, contents
):
276 file = open(path
, 'w')
280 class DiffParser(object):
281 def __init__(self
, model
,
282 filename
='', cached
=True):
283 self
.__header
_pattern
= re
.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
288 self
.__diff
_spans
= []
289 self
.__diff
_offsets
= []
298 model
.diff(filename
=filename
, with_diff_header
=True,
299 cached
=cached
, reverse
=cached
)
304 self
.parse_diff(diff
)
306 # Always index into the non-reversed diff
307 self
.fwd_header
, self
.fwd_diff
= \
308 model
.diff(filename
=filename
, with_diff_header
=True,
309 cached
=cached
, reverse
=False)
311 def write_diff(self
,filename
,which
,selected
=False,noop
=False):
312 if not noop
and which
< len(self
.diffs
):
313 diff
= self
.diffs
[which
]
314 write(filename
, self
.header
+ '\n' + diff
+ '\n')
322 def get_diff_subset(self
, diff
, start
, end
):
327 offset
= self
.__diff
_spans
[diff
][0]
328 diffguts
= '\n'.join(self
.__diffs
[diff
])
330 for line
in self
.__diffs
[diff
]:
331 line_start
= offset
+ local_offset
332 local_offset
+= len(line
) + 1 #\n
333 line_end
= offset
+ local_offset
334 # |line1 |line2 |line3 |
337 # selection has head of diff (line3)
338 if start
< line_start
and end
> line_start
and end
< line_end
:
340 if line
.startswith('+'):
342 if line
.startswith('-'):
344 # selection has all of diff (line2)
345 elif start
<= line_start
and end
>= line_end
:
347 if line
.startswith('+'):
349 if line
.startswith('-'):
351 # selection has tail of diff (line1)
352 elif start
>= line_start
and start
< line_end
- 1:
354 if line
.startswith('+'):
356 if line
.startswith('-'):
359 # Don't add new lines unless selected
360 if line
.startswith('+'):
362 elif line
.startswith('-'):
363 # Don't remove lines unless selected
364 newdiff
.append(' ' + line
[1:])
368 new_count
= self
.__headers
[diff
][1] + adds
- deletes
369 if new_count
!= self
.__headers
[diff
][3]:
370 header
= '@@ -%d,%d +%d,%d @@' % (
371 self
.__headers
[diff
][0],
372 self
.__headers
[diff
][1],
373 self
.__headers
[diff
][2],
377 return (self
.header
+ '\n' + '\n'.join(newdiff
) + '\n')
380 return self
.__diff
_spans
382 def get_offsets(self
):
383 return self
.__diff
_offsets
385 def set_diff_to_offset(self
, offset
):
387 self
.diffs
, self
.selected
= self
.get_diff_for_offset(offset
)
389 def set_diffs_to_range(self
, start
, end
):
392 self
.diffs
, self
.selected
= self
.get_diffs_for_range(start
,end
)
394 def get_diff_for_offset(self
, offset
):
395 for idx
, diff_offset
in enumerate(self
.__diff
_offsets
):
396 if offset
< diff_offset
:
397 return (['\n'.join(self
.__diffs
[idx
])], [idx
])
400 def get_diffs_for_range(self
, start
, end
):
403 for idx
, span
in enumerate(self
.__diff
_spans
):
404 has_end_of_diff
= start
>= span
[0] and start
< span
[1]
405 has_all_of_diff
= start
<= span
[0] and end
>= span
[1]
406 has_head_of_diff
= end
>= span
[0] and end
<= span
[1]
408 selected_diff
=(has_end_of_diff
412 diff
= '\n'.join(self
.__diffs
[idx
])
415 return diffs
, indices
417 def parse_diff(self
, diff
):
422 for idx
, line
in enumerate(diff
.splitlines()):
423 match
= self
.__header
_pattern
.match(line
)
425 self
.__headers
.append([
431 self
.__diffs
.append( [line
] )
433 line_len
= len(line
) + 1 #\n
434 self
.__diff
_spans
.append([total_offset
,
435 total_offset
+ line_len
])
436 total_offset
+= line_len
437 self
.__diff
_offsets
.append(total_offset
)
441 errmsg
= 'Malformed diff?\n\n%s' % diff
442 raise AssertionError, errmsg
443 line_len
= len(line
) + 1
444 total_offset
+= line_len
446 self
.__diffs
[self
.__idx
].append(line
)
447 self
.__diff
_spans
[-1][-1] += line_len
448 self
.__diff
_offsets
[self
.__idx
] += line_len
450 def process_diff_selection(self
, selected
, offset
, selection
):
452 start
= self
.fwd_diff
.index(selection
)
453 end
= start
+ len(selection
)
454 self
.set_diffs_to_range(start
, end
)
456 self
.set_diff_to_offset(offset
)
458 # Process diff selection only
460 for idx
in self
.selected
:
461 contents
= self
.get_diff_subset(idx
, start
, end
)
463 tmpfile
= get_tmp_filename()
464 write(tmpfile
, contents
)
465 self
.model
.apply_diff(tmpfile
)
467 # Process a complete hunk
469 for idx
, diff
in enumerate(self
.diffs
):
470 tmpfile
= get_tmp_filename()
471 if self
.write_diff(tmpfile
,idx
):
472 self
.model
.apply_diff(tmpfile
)