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
))
85 child
.setProcessChannelMode(QProcess
.MergedChannels
);
86 child
.start(cmd
[0], cmd
[1:])
88 if not child
.waitForStarted(): raise Exception("failed to start child")
89 if not child
.waitForFinished(): raise Exception("failed to start child")
91 output
= str(child
.readAll())
93 # Allow run_cmd(argv, raw=True) for when we
94 # want the full, raw output(e.g. git cat-file)
98 if 'with_status' in kwargs
:
99 return child
.exitCode(), output
.rstrip()
101 return output
.rstrip()
109 def grep(pattern
, items
, squash
=True):
110 if pattern
in __grep_cache
:
111 regex
= __grep_cache
[pattern
]
113 regex
= __grep_cache
[pattern
] = re
.compile(pattern
)
116 match
= regex
.match(item
)
117 if not match
: continue
118 groups
= match
.groups()
120 subitems
= match
.group(0)
125 subitems
= list(groups
)
126 matched
.append(subitems
)
128 if squash
and len(matched
) == 1:
134 '''Avoid os.path.basename because we are explicitly
135 parsing git's output, which contains /'s regardless
138 base_regex
= re
.compile('(.*?/)?([^/]+)$')
139 match
= base_regex
.match(path
)
141 return match
.group(2)
145 def shell_quote(*inputs
):
146 '''Quote strings so that they can be suitably martialled
147 off to the shell. This method supports POSIX sh syntax.
148 This is crucial to properly handle command line arguments
149 with spaces, quotes, double-quotes, etc.'''
151 regex
= re
.compile('[^\w!%+,\-./:@^]')
152 quote_regex
= re
.compile("((?:'\\''){2,})")
160 raise AssertionError,('No way to quote strings '
161 'containing null(\\000) bytes')
163 # = does need quoting else in command position it's a
164 # program-local environment setting
165 match
= regex
.search(input)
166 if match
and '=' not in input:
168 input = input.replace("'", "'\\''")
170 # make multiple ' in a row look simpler
171 # '\'''\'''\'' -> '"'''"'
172 quote_match
= quote_regex
.match(input)
174 quotes
= match
.group(1)
175 input.replace(quotes
,
176 ("'" *(len(quotes
)/4)) + "\"'")
178 input = "'%s'" % input
179 if input.startswith("''"):
182 if input.endswith("''"):
188 def get_tmp_filename():
189 # Allow TMPDIR/TMP with a fallback to /tmp
190 return '.ugit.%s.%s' %( os
.getpid(), time
.time() )
194 pad
= HEADER_LENGTH
- len(msg
) - 4 # len(':+') + len('+:')
200 +(' ' *(pad
+ extra
))
204 def parse_geom(geomstr
):
205 regex
= re
.compile('^(\d+)x(\d+)\+(\d+),(\d+) (\d+),(\d+) (\d+),(\d+)')
206 match
= regex
.match(geomstr
)
208 defaults
.WIDTH
= int(match
.group(1))
209 defaults
.HEIGHT
= int(match
.group(2))
210 defaults
.X
= int(match
.group(3))
211 defaults
.Y
= int(match
.group(4))
212 defaults
.SPLITTER_TOP_0
= int(match
.group(5))
213 defaults
.SPLITTER_TOP_1
= int(match
.group(6))
214 defaults
.SPLITTER_BOTTOM_0
= int(match
.group(7))
215 defaults
.SPLITTER_BOTTOM_1
= int(match
.group(8))
217 return (defaults
.WIDTH
, defaults
.HEIGHT
,
218 defaults
.X
, defaults
.Y
,
219 defaults
.SPLITTER_TOP_0
, defaults
.SPLITTER_TOP_1
,
220 defaults
.SPLITTER_BOTTOM_0
, defaults
.SPLITTER_BOTTOM_1
)
223 return '%dx%d+%d,%d %d,%d %d,%d' % (
224 defaults
.WIDTH
, defaults
.HEIGHT
,
225 defaults
.X
, defaults
.Y
,
226 defaults
.SPLITTER_TOP_0
, defaults
.SPLITTER_TOP_1
,
227 defaults
.SPLITTER_BOTTOM_0
, defaults
.SPLITTER_BOTTOM_1
)
230 return os
.path
.basename(defaults
.DIRECTORY
)
238 def write(path
, contents
):
239 file = open(path
, 'w')
244 class DiffParser(object):
245 def __init__(self
, model
,
246 filename
='', cached
=True):
247 self
.__header
_pattern
= re
.compile('^@@ -(\d+),(\d+) \+(\d+),(\d+) @@.*')
252 self
.__diff
_spans
= []
253 self
.__diff
_offsets
= []
262 model
.diff(filename
=filename
, with_diff_header
=True,
263 cached
=cached
, reverse
=cached
)
268 self
.parse_diff(diff
)
270 # Always index into the non-reversed diff
271 self
.fwd_header
, self
.fwd_diff
= \
272 model
.diff(filename
=filename
, with_diff_header
=True,
273 cached
=cached
, reverse
=False)
275 def write_diff(self
,filename
,which
,selected
=False,noop
=False):
276 if not noop
and which
< len(self
.diffs
):
277 diff
= self
.diffs
[which
]
278 write(filename
, self
.header
+ os
.linesep
+ diff
+ os
.linesep
)
286 def get_diff_subset(self
, diff
, start
, end
):
288 diffguts
= os
.linesep
.join(self
.__diffs
[diff
])
290 offset
= self
.__diff
_spans
[diff
][0]
297 for line
in self
.__diffs
[diff
]:
299 line_start
= offset
+ local_offset
300 local_offset
+= len(line
) + 1
301 line_end
= offset
+ local_offset
303 # |line1 |line2 |line3|
307 # selection has head of diff (line3)
308 if start
< line_start
and end
> line_start
and end
< line_end
:
310 if line
.startswith('+'):
312 if line
.startswith('-'):
314 # selection has all of diff (line2)
315 elif start
<= line_start
and end
>= line_end
:
317 if line
.startswith('+'):
319 if line
.startswith('-'):
321 # selection has tail of diff (line1)
322 elif start
>= line_start
and start
< line_end
- 1:
324 if line
.startswith('+'):
326 if line
.startswith('-'):
329 # Don't add new lines unless selected
330 if line
.startswith('+'):
332 elif line
.startswith('-'):
333 # Don't remove lines unless selected
334 newdiff
.append(' ' + line
[1:])
338 new_count
= self
.__headers
[diff
][1] + adds
- deletes
340 if new_count
!= self
.__headers
[diff
][3]:
341 header
= '@@ -%d,%d +%d,%d @@' % (
342 self
.__headers
[diff
][0],
343 self
.__headers
[diff
][1],
344 self
.__headers
[diff
][2],
350 + os
.linesep
.join(newdiff
)
354 return self
.__diff
_spans
356 def get_offsets(self
):
357 return self
.__diff
_offsets
359 def set_diff_to_offset(self
, offset
):
361 self
.diffs
, self
.selected
= self
.get_diff_for_offset(offset
)
363 def set_diffs_to_range(self
, start
, end
):
366 self
.diffs
, self
.selected
= self
.get_diffs_for_range(start
,end
)
368 def get_diff_for_offset(self
, offset
):
369 for idx
, diff_offset
in enumerate(self
.__diff
_offsets
):
370 if offset
< diff_offset
:
371 return ([os
.linesep
.join(self
.__diffs
[idx
])],
375 def get_diffs_for_range(self
, start
, end
):
378 for idx
, span
in enumerate(self
.__diff
_spans
):
379 has_end_of_diff
= start
>= span
[0] and start
< span
[1]
380 has_all_of_diff
= start
<= span
[0] and end
>= span
[1]
381 has_head_of_diff
= end
>= span
[0] and end
<= span
[1]
383 selected_diff
=(has_end_of_diff
388 diff
= os
.linesep
.join(self
.__diffs
[idx
])
391 return diffs
, indices
393 def parse_diff(self
, diff
):
398 for idx
, line
in enumerate(diff
.splitlines()):
400 match
= self
.__header
_pattern
.match(line
)
402 self
.__headers
.append([
409 self
.__diffs
.append( [line
] )
411 line_len
= len(line
) + 1
412 self
.__diff
_spans
.append([total_offset
,
413 total_offset
+ line_len
])
415 total_offset
+= line_len
416 self
.__diff
_offsets
.append(total_offset
)
421 errmsg
= 'Malformed diff?\n\n%s' % diff
422 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
):
433 start
= self
.fwd_diff
.index(selection
)
434 end
= start
+ len(selection
)
435 self
.set_diffs_to_range(start
, end
)
437 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
= get_tmp_filename()
446 write(tmpfile
, contents
)
447 self
.model
.apply_diff(tmpfile
)
449 # Process a complete hunk
451 for idx
, diff
in enumerate(self
.diffs
):
452 tmpfile
= get_tmp_filename()
453 if self
.write_diff(tmpfile
,idx
):
454 self
.model
.apply_diff(tmpfile
)