5 from cStringIO
import StringIO
7 from PyQt4
.QtCore
import QProcess
9 # A regex for matching the output of git (log|rev-list) --pretty=oneline
10 REV_LIST_REGEX
= re
.compile ('([0-9a-f]+)\W(.*)')
13 return ' '.join ([ utils
.shell_quote (arg
) for arg
in argv
])
15 def run_cmd (cmd
, *args
, **kwargs
):
16 # Handle cmd as either a string or an argv list
21 cmd
= list (cmd
+ list (args
))
24 child
.setProcessChannelMode(QProcess
.MergedChannels
);
25 child
.start (cmd
[0], cmd
[1:])
27 if (not child
.waitForStarted()):
28 raise Exception, "failed to start child"
30 if (not child
.waitForFinished()):
31 raise Exception, "failed to start child"
33 output
= str (child
.readAll())
35 # Allow run_cmd (argv, raw=True) for when we
36 # want the full, raw output (e.g. git cat-file)
37 if 'raw' in kwargs
and kwargs
['raw']:
40 return output
.rstrip()
43 '''Invokes 'git add' to index the filenames in to_add.'''
44 if not to_add
: return 'ERROR: No files to add.'
45 argv
= [ 'git', 'add' ]
47 return 'Running:\t' + quote (argv
) + '\n' + run_cmd (argv
)
49 def git_add_or_remove (to_process
):
50 '''Invokes 'git add' to index the filenames in to_process that exist
51 and 'git rm' for those that do not exist.'''
54 return 'ERROR: No files to add or remove.'
59 for filename
in to_process
:
60 if os
.path
.exists (filename
):
61 to_add
.append (filename
)
64 output
+= git_add (to_add
) + '\n\n'
66 if len(to_add
) == len(to_process
):
67 # to_process only contained unremoved files --
68 # short-circuit the removal checks
71 # Process files to add
72 argv
= [ 'git', 'rm' ]
73 for filename
in to_process
:
74 if not os
.path
.exists (filename
):
75 argv
.append (filename
)
77 return '%sRunning:\t%s\n%s' % ( output
, quote (argv
), run_cmd (argv
) )
79 def git_apply (filename
, indexonly
=True):
80 argv
= ['git', 'apply']
82 argv
.extend (['--index', '--cached'])
83 argv
.append (filename
)
86 def git_branch (name
=None, remote
=False, delete
=False):
87 argv
= ['git', 'branch']
89 return run_cmd (argv
, '-D', name
)
91 if remote
: argv
.append ('-r')
93 branches
= run_cmd (argv
).splitlines()
94 return map (lambda (x
): x
.lstrip ('* '), branches
)
96 def git_cat_file (objtype
, sha1
):
97 cmd
= 'git cat-file %s %s' % ( objtype
, sha1
)
98 return run_cmd (cmd
, raw
=True)
100 def git_cherry_pick (revs
, commit
=False):
101 '''Cherry-picks each revision into the current branch.'''
103 return 'ERROR: No revisions selected for cherry-picking.'
105 argv
= [ 'git', 'cherry-pick' ]
106 if not commit
: argv
.append ('-n')
110 output
.append ('Cherry-picking: ' + rev
)
111 output
.append (run_cmd (argv
, rev
))
113 return '\n'.join (output
)
115 def git_checkout(rev
):
116 return run_cmd('git','checkout', rev
)
118 def git_commit (msg
, amend
, files
):
119 '''Creates a git commit. 'commit_all' triggers the -a
120 flag to 'git commit.' 'amend' triggers --amend.
121 'files' is a list of files to use for commits without -a.'''
123 # Sure, this is a potential "security risk," but if someone
124 # is trying to intercept/re-write commit messages on your system,
125 # then you probably have bigger problems to worry about.
126 tmpfile
= utils
.get_tmp_filename()
127 argv
= [ 'git', 'commit', '-F', tmpfile
]
129 if amend
: argv
.append ('--amend')
132 return 'ERROR: No files selected for commit.'
137 # Create the commit message file
138 file = open (tmpfile
, 'w')
143 output
= run_cmd (argv
)
146 return 'Running:\t' + quote (argv
) + '\n\n' + output
148 def git_create_branch (name
, base
, track
=False):
149 '''Creates a branch starting from base. Pass track=True
150 to create a remote tracking branch.'''
151 argv
= ['git','branch']
152 if track
: argv
.append ('--track')
153 return run_cmd (argv
, name
, base
)
156 def git_current_branch():
157 '''Parses 'git branch' to find the current branch.'''
158 branches
= run_cmd ('git branch').splitlines()
159 for branch
in branches
:
160 if branch
.startswith ('* '):
161 return branch
.lstrip ('* ')
162 raise Exception, 'No current branch. Detached HEAD?'
164 def git_diff (filename
, staged
=True, color
=False, with_diff_header
=False):
165 '''Invokes git_diff on filename. Passing staged=True adds
166 diffs the index against HEAD (i.e. --cached).'''
169 argv
= [ 'git', 'diff']
171 argv
.append ('--color')
174 deleted
= not os
.path
.exists (filename
)
175 argv
.append ('--cached')
178 argv
.append (filename
)
180 diff
= run_cmd (argv
)
181 diff_lines
= diff
.splitlines()
185 del_tag
= 'deleted file mode '
188 for line
in diff_lines
:
189 if not start
and '@@ ' in line
and ' @@' in line
:
191 if start
or (deleted
and del_tag
in line
):
192 output
.write (line
+ '\n')
194 headers
.append (line
)
196 result
= output
.getvalue()
200 return (os
.linesep
.join (headers
), result
)
204 def git_diff_stat ():
205 '''Returns the latest diffstat.'''
206 return run_cmd ('git diff --stat HEAD^')
208 def git_format_patch (revs
, use_range
):
209 '''Exports patches revs in the 'ugit-patches' subdirectory.
210 If use_range is True, a commit range is passed to git format-patch.'''
212 argv
= ['git','format-patch','--thread','--patch-with-stat',
217 header
= 'Generated Patches:'
219 rev_range
= '%s^..%s' % ( revs
[-1], revs
[0] )
220 return (header
+ '\n'
221 + run_cmd (argv
, rev_range
))
225 for idx
, rev
in enumerate (revs
):
226 real_idx
= str (idx
+ num_patches
)
228 run_cmd (argv
, '-1', '--start-number', real_idx
, rev
))
230 num_patches
+= output
[-1].count ('\n')
232 return '\n'.join (output
)
234 def git_config(key
, value
=None):
235 '''Gets or sets git config values. If value is not None, then
236 the config key will be set. Otherwise, the config value of the
237 config key is returned.'''
238 if value
is not None:
239 return run_cmd ('git', 'config', key
, value
)
241 return run_cmd ('git', 'config', '--get', key
)
243 def git_log (oneline
=True, all
=False):
244 '''Returns a pair of parallel arrays listing the revision sha1's
245 and commit summaries.'''
246 argv
= [ 'git', 'log' ]
248 argv
.append ('--pretty=oneline')
250 argv
.append ('--all')
253 regex
= REV_LIST_REGEX
254 output
= run_cmd (argv
)
255 for line
in output
.splitlines():
256 match
= regex
.match (line
)
258 revs
.append (match
.group (1))
259 summaries
.append (match
.group (2))
260 return ( revs
, summaries
)
263 return run_cmd ('git ls-files').splitlines()
265 def git_ls_tree (rev
):
266 '''Returns a list of (mode, type, sha1, path) tuples.'''
268 lines
= run_cmd ('git', 'ls-tree', '-r', rev
).splitlines()
270 regex
= re
.compile ('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
272 match
= regex
.match (line
)
274 mode
= match
.group (1)
275 objtype
= match
.group (2)
276 sha1
= match
.group (3)
277 filename
= match
.group (4)
278 output
.append ( (mode
, objtype
, sha1
, filename
,) )
281 def git_rebase (newbase
):
282 if not newbase
: return
283 return run_cmd ('git','rebase', newbase
)
285 def git_reset (to_unstage
):
286 '''Use 'git reset' to unstage files from the index.'''
288 if not to_unstage
: return 'ERROR: No files to reset.'
290 argv
= [ 'git', 'reset', '--' ]
291 argv
.extend (to_unstage
)
293 return 'Running:\t' + quote (argv
) + '\n' + run_cmd (argv
)
295 def git_rev_list_range (start
, end
):
297 argv
= [ 'git', 'rev-list', '--pretty=oneline', start
, end
]
299 raw_revs
= run_cmd (argv
).splitlines()
301 regex
= REV_LIST_REGEX
302 for line
in raw_revs
:
303 match
= regex
.match (line
)
305 rev_id
= match
.group (1)
306 summary
= match
.group (2)
307 revs
.append ( (rev_id
, summary
,) )
311 def git_show (sha1
, color
=False):
313 if color
: cmd
+= '--color '
314 return run_cmd (cmd
+ sha1
)
317 '''Returns a relative path to the git project root.'''
318 return run_cmd ('git rev-parse --show-cdup')
321 '''RETURNS: A tuple of staged, unstaged and untracked files.
322 ( array(staged), array(unstaged), array(untracked) )'''
324 status_lines
= run_cmd ('git status').splitlines()
326 unstaged_header_seen
= False
327 untracked_header_seen
= False
329 modified_header
= '# Changed but not updated:'
330 modified_regex
= re
.compile ('(#\tmodified:\W{3}'
331 + '|#\tnew file:\W{3}'
332 + '|#\tdeleted:\W{4})')
334 renamed_regex
= re
.compile ('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
336 untracked_header
= '# Untracked files:'
337 untracked_regex
= re
.compile ('#\t(.+)')
344 for status_line
in status_lines
:
345 if untracked_header
in status_line
:
346 untracked_header_seen
= True
348 if not untracked_header_seen
:
350 match
= untracked_regex
.match (status_line
)
352 filename
= match
.group (1)
353 untracked
.append (filename
)
355 # Staged, unstaged, and renamed files
356 for status_line
in status_lines
:
357 if modified_header
in status_line
:
358 unstaged_header_seen
= True
360 match
= modified_regex
.match (status_line
)
362 tag
= match
.group (0)
363 filename
= status_line
.replace (tag
, '')
364 if unstaged_header_seen
:
365 unstaged
.append (filename
)
367 staged
.append (filename
)
370 match
= renamed_regex
.match (status_line
)
372 oldname
= match
.group (2)
373 newname
= match
.group (3)
374 staged
.append (oldname
)
375 staged
.append (newname
)
377 return ( staged
, unstaged
, untracked
)
380 return run_cmd ('git tag').splitlines()