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_branch (name
=None, remote
=False, delete
=False):
80 argv
= ['git', 'branch']
82 return run_cmd (argv
, '-D', name
)
84 if remote
: argv
.append ('-r')
86 branches
= run_cmd (argv
).splitlines()
87 return map (lambda (x
): x
.lstrip ('* '), branches
)
89 def git_cat_file (objtype
, sha1
):
90 cmd
= 'git cat-file %s %s' % ( objtype
, sha1
)
91 return run_cmd (cmd
, raw
=True)
93 def git_cherry_pick (revs
, commit
=False):
94 '''Cherry-picks each revision into the current branch.'''
96 return 'ERROR: No revisions selected for cherry-picking.'
98 argv
= [ 'git', 'cherry-pick' ]
99 if not commit
: argv
.append ('-n')
103 output
.append ('Cherry-picking: ' + rev
)
104 output
.append (run_cmd (argv
, rev
))
106 return '\n'.join (output
)
108 def git_checkout(rev
):
109 return run_cmd('git','checkout', rev
)
111 def git_commit (msg
, amend
, files
):
112 '''Creates a git commit. 'commit_all' triggers the -a
113 flag to 'git commit.' 'amend' triggers --amend.
114 'files' is a list of files to use for commits without -a.'''
116 # Sure, this is a potential "security risk," but if someone
117 # is trying to intercept/re-write commit messages on your system,
118 # then you probably have bigger problems to worry about.
119 tmpfile
= utils
.get_tmp_filename()
120 argv
= [ 'git', 'commit', '-F', tmpfile
]
122 if amend
: argv
.append ('--amend')
125 return 'ERROR: No files selected for commit.'
130 # Create the commit message file
131 file = open (tmpfile
, 'w')
136 output
= run_cmd (argv
)
139 return 'Running:\t' + quote (argv
) + '\n\n' + output
141 def git_create_branch (name
, base
, track
=False):
142 '''Creates a branch starting from base. Pass track=True
143 to create a remote tracking branch.'''
144 argv
= ['git','branch']
145 if track
: argv
.append ('--track')
146 return run_cmd (argv
, name
, base
)
149 def git_current_branch():
150 '''Parses 'git branch' to find the current branch.'''
151 branches
= run_cmd ('git branch').splitlines()
152 for branch
in branches
:
153 if branch
.startswith ('* '):
154 return branch
.lstrip ('* ')
155 raise Exception, 'No current branch. Detached HEAD?'
157 def git_diff (filename
, staged
=True, color
=False):
158 '''Invokes git_diff on filename. Passing staged=True adds
159 diffs the index against HEAD (i.e. --cached).'''
162 argv
= [ 'git', 'diff']
164 argv
.append ('--color')
167 deleted
= not os
.path
.exists (filename
)
168 argv
.append ('--cached')
171 argv
.append (filename
)
173 diff
= run_cmd (argv
)
174 diff_lines
= diff
.splitlines()
178 del_tag
= 'deleted file mode '
180 for line
in diff_lines
:
181 if not start
and '@@ ' in line
and ' @@' in line
:
183 if start
or (deleted
and del_tag
in line
):
184 output
.write (line
+ '\n')
185 return output
.getvalue()
187 def git_diff_stat ():
188 '''Returns the latest diffstat.'''
189 return run_cmd ('git diff --stat HEAD^')
191 def git_format_patch (revs
, use_range
):
192 '''Exports patches revs in the 'ugit-patches' subdirectory.
193 If use_range is True, a commit range is passed to git format-patch.'''
195 argv
= ['git','format-patch','--thread','--patch-with-stat',
200 header
= 'Generated Patches:'
202 rev_range
= '%s^..%s' % ( revs
[-1], revs
[0] )
203 return (header
+ '\n'
204 + run_cmd (argv
, rev_range
))
208 for idx
, rev
in enumerate (revs
):
209 real_idx
= str (idx
+ num_patches
)
211 run_cmd (argv
, '-1', '--start-number', real_idx
, rev
))
213 num_patches
+= output
[-1].count ('\n')
215 return '\n'.join (output
)
217 def git_config(key
, value
=None):
218 '''Gets or sets git config values. If value is not None, then
219 the config key will be set. Otherwise, the config value of the
220 config key is returned.'''
221 if value
is not None:
222 return run_cmd ('git', 'config', key
, value
)
224 return run_cmd ('git', 'config', '--get', key
)
226 def git_log (oneline
=True, all
=False):
227 '''Returns a pair of parallel arrays listing the revision sha1's
228 and commit summaries.'''
229 argv
= [ 'git', 'log' ]
231 argv
.append ('--pretty=oneline')
233 argv
.append ('--all')
236 regex
= REV_LIST_REGEX
237 output
= run_cmd (argv
)
238 for line
in output
.splitlines():
239 match
= regex
.match (line
)
241 revs
.append (match
.group (1))
242 summaries
.append (match
.group (2))
243 return ( revs
, summaries
)
246 return run_cmd ('git ls-files').splitlines()
248 def git_ls_tree (rev
):
249 '''Returns a list of (mode, type, sha1, path) tuples.'''
251 lines
= run_cmd ('git', 'ls-tree', '-r', rev
).splitlines()
253 regex
= re
.compile ('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
255 match
= regex
.match (line
)
257 mode
= match
.group (1)
258 objtype
= match
.group (2)
259 sha1
= match
.group (3)
260 filename
= match
.group (4)
261 output
.append ( (mode
, objtype
, sha1
, filename
,) )
264 def git_rebase (newbase
):
265 if not newbase
: return
266 return run_cmd ('git','rebase', newbase
)
268 def git_reset (to_unstage
):
269 '''Use 'git reset' to unstage files from the index.'''
271 if not to_unstage
: return 'ERROR: No files to reset.'
273 argv
= [ 'git', 'reset', '--' ]
274 argv
.extend (to_unstage
)
276 return 'Running:\t' + quote (argv
) + '\n' + run_cmd (argv
)
278 def git_rev_list_range (start
, end
):
280 argv
= [ 'git', 'rev-list', '--pretty=oneline', start
, end
]
282 raw_revs
= run_cmd (argv
).splitlines()
284 regex
= REV_LIST_REGEX
285 for line
in raw_revs
:
286 match
= regex
.match (line
)
288 rev_id
= match
.group (1)
289 summary
= match
.group (2)
290 revs
.append ( (rev_id
, summary
,) )
294 def git_show (sha1
, color
=False):
296 if color
: cmd
+= '--color '
297 return run_cmd (cmd
+ sha1
)
300 '''Returns a relative path to the git project root.'''
301 return run_cmd ('git rev-parse --show-cdup')
304 '''RETURNS: A tuple of staged, unstaged and untracked files.
305 ( array(staged), array(unstaged), array(untracked) )'''
307 status_lines
= run_cmd ('git status').splitlines()
309 unstaged_header_seen
= False
310 untracked_header_seen
= False
312 modified_header
= '# Changed but not updated:'
313 modified_regex
= re
.compile ('(#\tmodified:\W{3}'
314 + '|#\tnew file:\W{3}'
315 + '|#\tdeleted:\W{4})')
317 renamed_regex
= re
.compile ('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
319 untracked_header
= '# Untracked files:'
320 untracked_regex
= re
.compile ('#\t(.+)')
327 for status_line
in status_lines
:
328 if untracked_header
in status_line
:
329 untracked_header_seen
= True
331 if not untracked_header_seen
:
333 match
= untracked_regex
.match (status_line
)
335 filename
= match
.group (1)
336 untracked
.append (filename
)
338 # Staged, unstaged, and renamed files
339 for status_line
in status_lines
:
340 if modified_header
in status_line
:
341 unstaged_header_seen
= True
343 match
= modified_regex
.match (status_line
)
345 tag
= match
.group (0)
346 filename
= status_line
.replace (tag
, '')
347 if unstaged_header_seen
:
348 unstaged
.append (filename
)
350 staged
.append (filename
)
353 match
= renamed_regex
.match (status_line
)
355 oldname
= match
.group (2)
356 newname
= match
.group (3)
357 staged
.append (oldname
)
358 staged
.append (newname
)
360 return ( staged
, unstaged
, untracked
)
363 return run_cmd ('git tag').splitlines()