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("git foo", bar, buzz, baz=value, q=True)
74 argv=["git","foo","-q","--baz=value","bar","buzz"]
76 raw
= pop_key(kwargs
, 'raw')
77 with_status
= pop_key(kwargs
,'with_status')
78 with_stderr
= not pop_key(kwargs
,'without_stderr')
80 for k
,v
in kwargs
.iteritems():
82 k
= k
.replace('_','-')
84 kwarglist
.append("--%s" % k
)
85 elif v
is not None and type(v
) is not bool:
86 kwarglist
.append("--%s=%s" % (k
,v
))
89 kwarglist
.append("-%s" % k
)
90 elif v
is not None and type(v
) is not bool:
91 kwarglist
.append("-%s" % k
)
92 kwarglist
.append(str(v
))
93 # Handle cmd as either a string or an argv list
95 # we only call run_cmd(str) with str='git command'
96 # or other simple commands
101 cmd
= list(cmd
+ kwarglist
+ list(args
))
105 child
.setProcessChannelMode(QProcess
.MergedChannels
);
107 child
.start(cmd
[0], cmd
[1:])
108 if not (child
.waitForStarted() and child
.waitForFinished()):
110 output
= str(child
.readAll())
111 # run_cmd('cmd', *args, raw=True) if we want the full, raw output
114 return (child
.exitCode(), output
)
118 # simplify parsing by removing trailing \n from commands
120 return child
.exitCode(), output
[:-1]
138 def grep(pattern
, items
, squash
=True):
139 isdict
= type(items
) is dict
140 if pattern
in __grep_cache
:
141 regex
= __grep_cache
[pattern
]
143 regex
= __grep_cache
[pattern
] = re
.compile(pattern
)
147 match
= regex
.match(item
)
148 if not match
: continue
149 groups
= match
.groups()
151 subitems
= match
.group(0)
156 subitems
= list(groups
)
158 matchdict
[item
] = items
[item
]
160 matched
.append(subitems
)
165 if squash
and len(matched
) == 1:
171 '''Avoid os.path.basename because we are explicitly
172 parsing git's output, which contains /'s regardless
175 base_regex
= re
.compile('(.*?/)?([^/]+)$')
176 match
= base_regex
.match(path
)
178 return match
.group(2)
182 def shell_quote(*inputs
):
183 '''Quote strings so that they can be suitably martialled
184 off to the shell. This method supports POSIX sh syntax.
185 This is crucial to properly handle command line arguments
186 with spaces, quotes, double-quotes, etc.'''
188 regex
= re
.compile('[^\w!%+,\-./:@^]')
189 quote_regex
= re
.compile("((?:'\\''){2,})")
197 raise AssertionError,('No way to quote strings '
198 'containing null(\\000) bytes')
200 # = does need quoting else in command position it's a
201 # program-local environment setting
202 match
= regex
.search(input)
203 if match
and '=' not in input:
205 input = input.replace("'", "'\\''")
207 # make multiple ' in a row look simpler
208 # '\'''\'''\'' -> '"'''"'
209 quote_match
= quote_regex
.match(input)
211 quotes
= match
.group(1)
212 input.replace(quotes
,
213 ("'" *(len(quotes
)/4)) + "\"'")
215 input = "'%s'" % input
216 if input.startswith("''"):
219 if input.endswith("''"):
224 def get_tmp_filename():
225 # Allow TMPDIR/TMP with a fallback to /tmp
226 return '.ugit.%s.%s' %( os
.getpid(), time
.time() )
230 pad
= HEADER_LENGTH
- len(msg
) - 4 # len(':+') + len('+:')
236 +(' ' *(pad
+ extra
))
240 def parse_geom(geomstr
):
241 regex
= re
.compile('^(\d+)x(\d+)\+(\d+),(\d+) (\d+),(\d+) (\d+),(\d+)')
242 match
= regex
.match(geomstr
)
244 defaults
.WIDTH
= int(match
.group(1))
245 defaults
.HEIGHT
= int(match
.group(2))
246 defaults
.X
= int(match
.group(3))
247 defaults
.Y
= int(match
.group(4))
248 defaults
.SPLITTER_TOP_0
= int(match
.group(5))
249 defaults
.SPLITTER_TOP_1
= int(match
.group(6))
250 defaults
.SPLITTER_BOTTOM_0
= int(match
.group(7))
251 defaults
.SPLITTER_BOTTOM_1
= int(match
.group(8))
253 return (defaults
.WIDTH
, defaults
.HEIGHT
,
254 defaults
.X
, defaults
.Y
,
255 defaults
.SPLITTER_TOP_0
, defaults
.SPLITTER_TOP_1
,
256 defaults
.SPLITTER_BOTTOM_0
, defaults
.SPLITTER_BOTTOM_1
)
259 return '%dx%d+%d,%d %d,%d %d,%d' % (
260 defaults
.WIDTH
, defaults
.HEIGHT
,
261 defaults
.X
, defaults
.Y
,
262 defaults
.SPLITTER_TOP_0
, defaults
.SPLITTER_TOP_1
,
263 defaults
.SPLITTER_BOTTOM_0
, defaults
.SPLITTER_BOTTOM_1
)
266 return os
.path
.basename(defaults
.DIRECTORY
)
274 def write(path
, contents
):
275 file = open(path
, 'w')
279 class DiffParser(object):
280 def __init__(self
, model
,
281 filename
='', cached
=True):
282 self
.__header
_pattern
= re
.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
287 self
.__diff
_spans
= []
288 self
.__diff
_offsets
= []
299 with_diff_header
=True,
306 self
.parse_diff(diff
)
308 # Always index into the non-reversed diff
309 self
.fwd_header
, self
.fwd_diff
= \
312 with_diff_header
=True,
317 def write_diff(self
,filename
,which
,selected
=False,noop
=False):
318 if not noop
and which
< len(self
.diffs
):
319 diff
= self
.diffs
[which
]
320 write(filename
, self
.header
+ '\n' + diff
+ '\n')
328 def get_diff_subset(self
, diff
, start
, end
):
333 offset
= self
.__diff
_spans
[diff
][0]
334 diffguts
= '\n'.join(self
.__diffs
[diff
])
336 for line
in self
.__diffs
[diff
]:
337 line_start
= offset
+ local_offset
338 local_offset
+= len(line
) + 1 #\n
339 line_end
= offset
+ local_offset
340 # |line1 |line2 |line3 |
343 # selection has head of diff (line3)
344 if start
< line_start
and end
> line_start
and end
< line_end
:
346 if line
.startswith('+'):
348 if line
.startswith('-'):
350 # selection has all of diff (line2)
351 elif start
<= line_start
and end
>= line_end
:
353 if line
.startswith('+'):
355 if line
.startswith('-'):
357 # selection has tail of diff (line1)
358 elif start
>= line_start
and start
< line_end
- 1:
360 if line
.startswith('+'):
362 if line
.startswith('-'):
365 # Don't add new lines unless selected
366 if line
.startswith('+'):
368 elif line
.startswith('-'):
369 # Don't remove lines unless selected
370 newdiff
.append(' ' + line
[1:])
374 new_count
= self
.__headers
[diff
][1] + adds
- deletes
375 if new_count
!= self
.__headers
[diff
][3]:
376 header
= '@@ -%d,%d +%d,%d @@' % (
377 self
.__headers
[diff
][0],
378 self
.__headers
[diff
][1],
379 self
.__headers
[diff
][2],
383 return (self
.header
+ '\n' + '\n'.join(newdiff
) + '\n')
386 return self
.__diff
_spans
388 def get_offsets(self
):
389 return self
.__diff
_offsets
391 def set_diff_to_offset(self
, offset
):
393 self
.diffs
, self
.selected
= self
.get_diff_for_offset(offset
)
395 def set_diffs_to_range(self
, start
, end
):
398 self
.diffs
, self
.selected
= self
.get_diffs_for_range(start
,end
)
400 def get_diff_for_offset(self
, offset
):
401 for idx
, diff_offset
in enumerate(self
.__diff
_offsets
):
402 if offset
< diff_offset
:
403 return (['\n'.join(self
.__diffs
[idx
])], [idx
])
406 def get_diffs_for_range(self
, start
, end
):
409 for idx
, span
in enumerate(self
.__diff
_spans
):
410 has_end_of_diff
= start
>= span
[0] and start
< span
[1]
411 has_all_of_diff
= start
<= span
[0] and end
>= span
[1]
412 has_head_of_diff
= end
>= span
[0] and end
<= span
[1]
414 selected_diff
=(has_end_of_diff
418 diff
= '\n'.join(self
.__diffs
[idx
])
421 return diffs
, indices
423 def parse_diff(self
, diff
):
428 for idx
, line
in enumerate(diff
.splitlines()):
429 match
= self
.__header
_pattern
.match(line
)
431 self
.__headers
.append([
437 self
.__diffs
.append( [line
] )
439 line_len
= len(line
) + 1 #\n
440 self
.__diff
_spans
.append([total_offset
,
441 total_offset
+ line_len
])
442 total_offset
+= line_len
443 self
.__diff
_offsets
.append(total_offset
)
447 errmsg
= 'Malformed diff?\n\n%s' % diff
448 raise AssertionError, errmsg
449 line_len
= len(line
) + 1
450 total_offset
+= line_len
452 self
.__diffs
[self
.__idx
].append(line
)
453 self
.__diff
_spans
[-1][-1] += line_len
454 self
.__diff
_offsets
[self
.__idx
] += line_len
456 def process_diff_selection(self
, selected
, offset
, selection
):
458 start
= self
.fwd_diff
.index(selection
)
459 end
= start
+ len(selection
)
460 self
.set_diffs_to_range(start
, end
)
462 self
.set_diff_to_offset(offset
)
464 # Process diff selection only
466 for idx
in self
.selected
:
467 contents
= self
.get_diff_subset(idx
, start
, end
)
469 tmpfile
= get_tmp_filename()
470 write(tmpfile
, contents
)
471 self
.model
.apply_diff(tmpfile
)
473 # Process a complete hunk
475 for idx
, diff
in enumerate(self
.diffs
):
476 tmpfile
= get_tmp_filename()
477 if self
.write_diff(tmpfile
,idx
):
478 self
.model
.apply_diff(tmpfile
)