6 from cStringIO
import StringIO
8 from PyQt4
.QtCore
import QProcess
12 PREFIX
= os
.path
.realpath(os
.path
.dirname(os
.path
.dirname(sys
.argv
[0])))
13 QMDIR
= os
.path
.join(PREFIX
, 'share', 'ugit', 'qm')
15 def get_qm_for_locale(locale
):
16 regex
= re
.compile(r
'([^\.])+\..*$')
17 match
= regex
.match(locale
)
19 locale
= match
.group(1)
21 basename
= locale
.split('_')[0]
23 return os
.path
.join(QMDIR
, basename
+'.qm')
26 ICONSDIR
= os
.path
.join(PREFIX
, 'share', 'ugit', 'icons')
29 'python': 'script.png',
31 'shell': 'script.png',
34 'assembler': 'binary.png',
35 'binary': 'binary.png',
40 def ident_file_type(filename
):
41 '''Returns an icon based on the contents of filename.'''
42 if os
.path
.exists(filename
):
43 fileinfo
= run_cmd('file','-b',filename
)
44 for filetype
, iconname
in KNOWN_FILE_TYPES
.iteritems():
45 if filetype
in fileinfo
.lower():
49 # Fallback for modified files of an unknown type
52 def get_icon(filename
):
53 '''Returns the full path to an icon file corresponding to
54 filename's contents.'''
55 icon_file
= ident_file_type(filename
)
56 return os
.path
.join(ICONSDIR
, icon_file
)
58 def get_staged_icon(filename
):
59 '''Special-case method for staged items. These are only
60 ever 'staged' and 'removed' items in the staged list.'''
62 if os
.path
.exists(filename
):
63 return os
.path
.join(ICONSDIR
, 'staged.png')
65 return os
.path
.join(ICONSDIR
, 'removed.png')
67 def get_untracked_icon():
68 return os
.path
.join(ICONSDIR
, 'untracked.png')
70 def get_directory_icon():
71 return os
.path
.join(ICONSDIR
, 'dir.png')
74 return os
.path
.join(ICONSDIR
, 'generic.png')
76 def run_cmd(cmd
, *args
, **kwargs
):
77 # Handle cmd as either a string or an argv list
82 cmd
= list(cmd
+ list(args
))
86 if 'stderr' not in kwargs
:
87 child
.setProcessChannelMode(QProcess
.MergedChannels
);
89 child
.start(cmd
[0], cmd
[1:])
91 if not child
.waitForStarted(): return ''
92 if not child
.waitForFinished(): return ''
94 output
= str(child
.readAll())
96 # run_cmd(argv, raw=True) if we want the full, raw output
100 if 'with_status' in kwargs
:
101 return child
.exitCode(), output
.rstrip()
103 return output
.rstrip()
119 def grep(pattern
, items
, squash
=True):
120 isdict
= type(items
) is dict
121 if pattern
in __grep_cache
:
122 regex
= __grep_cache
[pattern
]
124 regex
= __grep_cache
[pattern
] = re
.compile(pattern
)
128 match
= regex
.match(item
)
129 if not match
: continue
130 groups
= match
.groups()
132 subitems
= match
.group(0)
137 subitems
= list(groups
)
139 matchdict
[item
] = items
[item
]
141 matched
.append(subitems
)
146 if squash
and len(matched
) == 1:
152 '''Avoid os.path.basename because we are explicitly
153 parsing git's output, which contains /'s regardless
156 base_regex
= re
.compile('(.*?/)?([^/]+)$')
157 match
= base_regex
.match(path
)
159 return match
.group(2)
163 def shell_quote(*inputs
):
164 '''Quote strings so that they can be suitably martialled
165 off to the shell. This method supports POSIX sh syntax.
166 This is crucial to properly handle command line arguments
167 with spaces, quotes, double-quotes, etc.'''
169 regex
= re
.compile('[^\w!%+,\-./:@^]')
170 quote_regex
= re
.compile("((?:'\\''){2,})")
178 raise AssertionError,('No way to quote strings '
179 'containing null(\\000) bytes')
181 # = does need quoting else in command position it's a
182 # program-local environment setting
183 match
= regex
.search(input)
184 if match
and '=' not in input:
186 input = input.replace("'", "'\\''")
188 # make multiple ' in a row look simpler
189 # '\'''\'''\'' -> '"'''"'
190 quote_match
= quote_regex
.match(input)
192 quotes
= match
.group(1)
193 input.replace(quotes
,
194 ("'" *(len(quotes
)/4)) + "\"'")
196 input = "'%s'" % input
197 if input.startswith("''"):
200 if input.endswith("''"):
206 def get_tmp_filename():
207 # Allow TMPDIR/TMP with a fallback to /tmp
208 return '.ugit.%s.%s' %( os
.getpid(), time
.time() )
212 pad
= HEADER_LENGTH
- len(msg
) - 4 # len(':+') + len('+:')
218 +(' ' *(pad
+ extra
))
222 def parse_geom(geomstr
):
223 regex
= re
.compile('^(\d+)x(\d+)\+(\d+),(\d+) (\d+),(\d+) (\d+),(\d+)')
224 match
= regex
.match(geomstr
)
226 defaults
.WIDTH
= int(match
.group(1))
227 defaults
.HEIGHT
= int(match
.group(2))
228 defaults
.X
= int(match
.group(3))
229 defaults
.Y
= int(match
.group(4))
230 defaults
.SPLITTER_TOP_0
= int(match
.group(5))
231 defaults
.SPLITTER_TOP_1
= int(match
.group(6))
232 defaults
.SPLITTER_BOTTOM_0
= int(match
.group(7))
233 defaults
.SPLITTER_BOTTOM_1
= int(match
.group(8))
235 return (defaults
.WIDTH
, defaults
.HEIGHT
,
236 defaults
.X
, defaults
.Y
,
237 defaults
.SPLITTER_TOP_0
, defaults
.SPLITTER_TOP_1
,
238 defaults
.SPLITTER_BOTTOM_0
, defaults
.SPLITTER_BOTTOM_1
)
241 return '%dx%d+%d,%d %d,%d %d,%d' % (
242 defaults
.WIDTH
, defaults
.HEIGHT
,
243 defaults
.X
, defaults
.Y
,
244 defaults
.SPLITTER_TOP_0
, defaults
.SPLITTER_TOP_1
,
245 defaults
.SPLITTER_BOTTOM_0
, defaults
.SPLITTER_BOTTOM_1
)
248 return os
.path
.basename(defaults
.DIRECTORY
)
256 def write(path
, contents
):
257 file = open(path
, 'w')
262 class DiffParser(object):
263 def __init__(self
, model
,
264 filename
='', cached
=True):
265 self
.__header
_pattern
= re
.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
270 self
.__diff
_spans
= []
271 self
.__diff
_offsets
= []
280 model
.diff(filename
=filename
, with_diff_header
=True,
281 cached
=cached
, reverse
=cached
)
286 self
.parse_diff(diff
)
288 # Always index into the non-reversed diff
289 self
.fwd_header
, self
.fwd_diff
= \
290 model
.diff(filename
=filename
, with_diff_header
=True,
291 cached
=cached
, reverse
=False)
293 def write_diff(self
,filename
,which
,selected
=False,noop
=False):
294 if not noop
and which
< len(self
.diffs
):
295 diff
= self
.diffs
[which
]
296 write(filename
, self
.header
+ os
.linesep
+ diff
+ os
.linesep
)
304 def get_diff_subset(self
, diff
, start
, end
):
306 diffguts
= os
.linesep
.join(self
.__diffs
[diff
])
308 offset
= self
.__diff
_spans
[diff
][0]
315 for line
in self
.__diffs
[diff
]:
317 line_start
= offset
+ local_offset
318 local_offset
+= len(line
) + 1
319 line_end
= offset
+ local_offset
321 # |line1 |line2 |line3|
325 # selection has head of diff (line3)
326 if start
< line_start
and end
> line_start
and end
< line_end
:
328 if line
.startswith('+'):
330 if line
.startswith('-'):
332 # selection has all of diff (line2)
333 elif start
<= line_start
and end
>= line_end
:
335 if line
.startswith('+'):
337 if line
.startswith('-'):
339 # selection has tail of diff (line1)
340 elif start
>= line_start
and start
< line_end
- 1:
342 if line
.startswith('+'):
344 if line
.startswith('-'):
347 # Don't add new lines unless selected
348 if line
.startswith('+'):
350 elif line
.startswith('-'):
351 # Don't remove lines unless selected
352 newdiff
.append(' ' + line
[1:])
356 new_count
= self
.__headers
[diff
][1] + adds
- deletes
358 if new_count
!= self
.__headers
[diff
][3]:
359 header
= '@@ -%d,%d +%d,%d @@' % (
360 self
.__headers
[diff
][0],
361 self
.__headers
[diff
][1],
362 self
.__headers
[diff
][2],
368 + os
.linesep
.join(newdiff
)
372 return self
.__diff
_spans
374 def get_offsets(self
):
375 return self
.__diff
_offsets
377 def set_diff_to_offset(self
, offset
):
379 self
.diffs
, self
.selected
= self
.get_diff_for_offset(offset
)
381 def set_diffs_to_range(self
, start
, end
):
384 self
.diffs
, self
.selected
= self
.get_diffs_for_range(start
,end
)
386 def get_diff_for_offset(self
, offset
):
387 for idx
, diff_offset
in enumerate(self
.__diff
_offsets
):
388 if offset
< diff_offset
:
389 return ([os
.linesep
.join(self
.__diffs
[idx
])],
393 def get_diffs_for_range(self
, start
, end
):
396 for idx
, span
in enumerate(self
.__diff
_spans
):
397 has_end_of_diff
= start
>= span
[0] and start
< span
[1]
398 has_all_of_diff
= start
<= span
[0] and end
>= span
[1]
399 has_head_of_diff
= end
>= span
[0] and end
<= span
[1]
401 selected_diff
=(has_end_of_diff
406 diff
= os
.linesep
.join(self
.__diffs
[idx
])
409 return diffs
, indices
411 def parse_diff(self
, diff
):
416 for idx
, line
in enumerate(diff
.splitlines()):
418 match
= self
.__header
_pattern
.match(line
)
420 self
.__headers
.append([
427 self
.__diffs
.append( [line
] )
429 line_len
= len(line
) + 1
430 self
.__diff
_spans
.append([total_offset
,
431 total_offset
+ line_len
])
433 total_offset
+= line_len
434 self
.__diff
_offsets
.append(total_offset
)
439 errmsg
= 'Malformed diff?\n\n%s' % diff
440 raise AssertionError, errmsg
442 line_len
= len(line
) + 1
443 total_offset
+= line_len
445 self
.__diffs
[self
.__idx
].append(line
)
446 self
.__diff
_spans
[-1][-1] += line_len
447 self
.__diff
_offsets
[self
.__idx
] += line_len
449 def process_diff_selection(self
, selected
, offset
, selection
):
451 start
= self
.fwd_diff
.index(selection
)
452 end
= start
+ len(selection
)
453 self
.set_diffs_to_range(start
, end
)
455 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
)