5 from cStringIO
import StringIO
7 from PyQt4
.QtCore
import QProcess
8 from PyQt4
.QtCore
import QObject
11 # A regex for matching the output of git(log|rev-list) --pretty=oneline
12 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
15 return ' '.join([ utils
.shell_quote(arg
) for arg
in argv
])
17 def run_cmd(cmd
, *args
, **kwargs
):
18 # Handle cmd as either a string or an argv list
23 cmd
= list(cmd
+ list(args
))
26 child
.setProcessChannelMode(QProcess
.MergedChannels
);
27 child
.start(cmd
[0], cmd
[1:])
29 if(not child
.waitForStarted()):
30 raise Exception("failed to start child")
32 if(not child
.waitForFinished()):
33 raise Exception("failed to start child")
35 output
= str(child
.readAll())
37 # Allow run_cmd(argv, raw=True) for when we
38 # want the full, raw output(e.g. git cat-file)
39 if 'raw' in kwargs
and kwargs
['raw']:
42 if 'with_status' in kwargs
:
43 return child
.exitCode(), output
.rstrip()
45 return output
.rstrip()
48 '''Invokes 'git add' to index the filenames in to_add.'''
49 if not to_add
: return 'No files to add.'
50 argv
= [ 'git', 'add' ]
52 return quote(argv
) + '\n' + run_cmd(argv
)
54 def git_add_or_remove(to_process
):
55 '''Invokes 'git add' to index the filenames in to_process that exist
56 and 'git rm' for those that do not exist.'''
59 return 'No files to add or remove.'
64 for filename
in to_process
:
65 if os
.path
.exists(filename
):
66 to_add
.append(filename
)
70 if len(to_add
) == len(to_process
):
71 # to_process only contained unremoved files --
72 # short-circuit the removal checks
75 # Process files to remote
76 argv
= [ 'git', 'rm' ]
77 for filename
in to_process
:
78 if not os
.path
.exists(filename
):
84 def git_apply(filename
, indexonly
=True):
85 argv
= ['git', 'apply']
87 argv
.extend(['--index', '--cached'])
91 def git_branch(name
=None, remote
=False, delete
=False):
92 argv
= ['git', 'branch']
94 return run_cmd(argv
, '-D', name
)
96 if remote
: argv
.append('-r')
98 branches
= run_cmd(argv
).splitlines()
99 return map(lambda(x
): x
.lstrip('* '), branches
)
101 def git_cat_file(objtype
, sha1
):
102 cmd
= 'git cat-file %s %s' %( objtype
, sha1
)
103 return run_cmd(cmd
, raw
=True)
105 def git_cherry_pick(revs
, commit
=False):
106 '''Cherry-picks each revision into the current branch.'''
108 return 'No revision selected.'
110 argv
= [ 'git', 'cherry-pick' ]
111 if not commit
: argv
.append('-n')
115 output
.append('Cherry picking:' + ' '+ rev
)
116 output
.append(run_cmd(argv
, rev
))
118 return '\n'.join(output
)
120 def git_checkout(rev
):
121 return run_cmd('git','checkout', rev
)
123 def git_commit(msg
, amend
, files
):
124 '''Creates a git commit. 'commit_all' triggers the -a
125 flag to 'git commit.' 'amend' triggers --amend.
126 'files' is a list of files to use for commits without -a.'''
128 # Sure, this is a potential "security risk," but if someone
129 # is trying to intercept/re-write commit messages on your system,
130 # then you probably have bigger problems to worry about.
131 tmpfile
= utils
.get_tmp_filename()
132 argv
= [ 'git', 'commit', '-F', tmpfile
]
134 if amend
: argv
.append('--amend')
137 return 'No files selected.'
142 # Create the commit message file
143 file = open(tmpfile
, 'w')
148 output
= run_cmd(argv
)
151 return quote(argv
) + '\n\n' + output
153 def git_create_branch(name
, base
, track
=False):
154 '''Creates a branch starting from base. Pass track=True
155 to create a remote tracking branch.'''
156 argv
= ['git','branch']
157 if track
: argv
.append('--track')
158 return run_cmd(argv
, name
, base
)
161 def git_current_branch():
162 '''Parses 'git branch' to find the current branch.'''
163 branches
= run_cmd('git branch').splitlines()
164 for branch
in branches
:
165 if branch
.startswith('* '):
166 return branch
.lstrip('* ')
168 return 'Detached HEAD'
170 def git_diff(filename
, staged
=True, color
=False, with_diff_header
=False):
171 '''Invokes git_diff on filename. Passing staged=True adds
172 diffs the index against HEAD(i.e. --cached).'''
175 argv
= [ 'git', 'diff']
177 argv
.append('--color')
180 deleted
= not os
.path
.exists(filename
)
181 argv
.append('--cached')
184 argv
.append(filename
)
187 diff_lines
= diff
.splitlines()
191 del_tag
= 'deleted file mode '
194 for line
in diff_lines
:
195 if not start
and '@@ ' in line
and ' @@' in line
:
197 if start
or(deleted
and del_tag
in line
):
198 output
.write(line
+ '\n')
202 result
= output
.getvalue()
206 return(os
.linesep
.join(headers
), result
)
211 '''Returns the latest diffstat.'''
212 return run_cmd('git diff --stat HEAD^')
214 def git_format_patch(revs
, use_range
):
215 '''Exports patches revs in the 'ugit-patches' subdirectory.
216 If use_range is True, a commit range is passed to git format-patch.'''
218 argv
= ['git','format-patch','--thread','--patch-with-stat',
223 header
= 'Generated Patches:'
225 rev_range
= '%s^..%s' %( revs
[-1], revs
[0] )
227 + run_cmd(argv
, rev_range
))
231 for idx
, rev
in enumerate(revs
):
232 real_idx
= str(idx
+ num_patches
)
234 run_cmd(argv
, '-1', '--start-number', real_idx
, rev
))
236 num_patches
+= output
[-1].count('\n')
238 return '\n'.join(output
)
240 def git_config(key
, value
=None):
241 '''Gets or sets git config values. If value is not None, then
242 the config key will be set. Otherwise, the config value of the
243 config key is returned.'''
244 if value
is not None:
245 return run_cmd('git', 'config', key
, value
)
247 return run_cmd('git', 'config', '--get', key
)
249 def git_log(oneline
=True, all
=False):
250 '''Returns a pair of parallel arrays listing the revision sha1's
251 and commit summaries.'''
252 argv
= [ 'git', 'log' ]
254 argv
.append('--pretty=oneline')
259 regex
= REV_LIST_REGEX
260 output
= run_cmd(argv
)
261 for line
in output
.splitlines():
262 match
= regex
.match(line
)
264 revs
.append(match
.group(1))
265 summaries
.append(match
.group(2))
266 return( revs
, summaries
)
269 return run_cmd('git ls-files').splitlines()
271 def git_ls_tree(rev
):
272 '''Returns a list of(mode, type, sha1, path) tuples.'''
274 lines
= run_cmd('git', 'ls-tree', '-r', rev
).splitlines()
276 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
278 match
= regex
.match(line
)
280 mode
= match
.group(1)
281 objtype
= match
.group(2)
282 sha1
= match
.group(3)
283 filename
= match
.group(4)
284 output
.append((mode
, objtype
, sha1
, filename
,) )
287 def git_push(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
288 argv
= ['git', 'push']
290 argv
.append('--tags')
293 if local_branch
== remote_branch
:
294 argv
.append(local_branch
)
296 if not ffwd
and local_branch
:
297 argv
.append('+%s:%s' % ( local_branch
, remote_branch
))
299 argv
.append('%s:%s' % ( local_branch
, remote_branch
))
301 return run_cmd(argv
, with_status
=True)
303 def git_rebase(newbase
):
304 if not newbase
: return
305 return run_cmd('git','rebase', newbase
)
307 def git_remote(*args
):
308 return run_cmd('git','remote',*args
).splitlines()
310 def git_remote_show(remote
):
312 for line
in git_remote('show',remote
):
313 info
.append(line
.strip())
316 def git_remote_url(remote
):
317 return utils
.grep('^URL:\s+(.*)', git_remote_show(remote
))
319 def git_reset(to_unstage
):
320 '''Use 'git reset' to unstage files from the index.'''
323 return 'No files to reset.'
325 argv
= [ 'git', 'reset', '--' ]
326 argv
.extend(to_unstage
)
328 return quote(argv
) + '\n' + run_cmd(argv
)
330 def git_rev_list_range(start
, end
):
332 argv
= [ 'git', 'rev-list', '--pretty=oneline', start
, end
]
334 raw_revs
= run_cmd(argv
).splitlines()
336 regex
= REV_LIST_REGEX
337 for line
in raw_revs
:
338 match
= regex
.match(line
)
340 rev_id
= match
.group(1)
341 summary
= match
.group(2)
342 revs
.append((rev_id
, summary
,) )
346 def git_show(sha1
, color
=False):
348 if color
: cmd
+= '--color '
349 return run_cmd(cmd
+ sha1
)
352 '''Returns a relative path to the git project root.'''
353 return run_cmd('git rev-parse --show-cdup')
356 '''RETURNS: A tuple of staged, unstaged and untracked files.
357 ( array(staged), array(unstaged), array(untracked) )'''
359 status_lines
= run_cmd('git status').splitlines()
361 unstaged_header_seen
= False
362 untracked_header_seen
= False
364 modified_header
= '# Changed but not updated:'
365 modified_regex
= re
.compile('(#\tmodified:\W{3}'
366 + '|#\tnew file:\W{3}'
367 + '|#\tdeleted:\W{4})')
369 renamed_regex
= re
.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
371 untracked_header
= '# Untracked files:'
372 untracked_regex
= re
.compile('#\t(.+)')
379 for status_line
in status_lines
:
380 if untracked_header
in status_line
:
381 untracked_header_seen
= True
383 if not untracked_header_seen
:
385 match
= untracked_regex
.match(status_line
)
387 filename
= match
.group(1)
388 untracked
.append(filename
)
390 # Staged, unstaged, and renamed files
391 for status_line
in status_lines
:
392 if modified_header
in status_line
:
393 unstaged_header_seen
= True
395 match
= modified_regex
.match(status_line
)
398 filename
= status_line
.replace(tag
, '')
399 if unstaged_header_seen
:
400 unstaged
.append(filename
)
402 staged
.append(filename
)
405 match
= renamed_regex
.match(status_line
)
407 oldname
= match
.group(2)
408 newname
= match
.group(3)
409 staged
.append(oldname
)
410 staged
.append(newname
)
412 return( staged
, unstaged
, untracked
)
415 return run_cmd('git tag').splitlines()