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 if 'with_status' in kwargs
:
41 return child
.exitCode(), output
.rstrip()
43 return output
.rstrip()
46 '''Invokes 'git add' to index the filenames in to_add.'''
47 if not to_add
: return 'ERROR: No files to add.'
48 argv
= [ 'git', 'add' ]
50 return 'Running:\t' + quote(argv
) + '\n' + run_cmd(argv
)
52 def git_add_or_remove(to_process
):
53 '''Invokes 'git add' to index the filenames in to_process that exist
54 and 'git rm' for those that do not exist.'''
57 return 'ERROR: No files to add or remove.'
62 for filename
in to_process
:
63 if os
.path
.exists(filename
):
64 to_add
.append(filename
)
67 output
+= git_add(to_add
) + '\n\n'
69 if len(to_add
) == len(to_process
):
70 # to_process only contained unremoved files --
71 # short-circuit the removal checks
74 # Process files to add
75 argv
= [ 'git', 'rm' ]
76 for filename
in to_process
:
77 if not os
.path
.exists(filename
):
80 return '%sRunning:\t%s\n%s' %( output
, quote(argv
), run_cmd(argv
) )
82 def git_apply(filename
, indexonly
=True):
83 argv
= ['git', 'apply']
85 argv
.extend(['--index', '--cached'])
89 def git_branch(name
=None, remote
=False, delete
=False):
90 argv
= ['git', 'branch']
92 return run_cmd(argv
, '-D', name
)
94 if remote
: argv
.append('-r')
96 branches
= run_cmd(argv
).splitlines()
97 return map(lambda(x
): x
.lstrip('* '), branches
)
99 def git_cat_file(objtype
, sha1
):
100 cmd
= 'git cat-file %s %s' %( objtype
, sha1
)
101 return run_cmd(cmd
, raw
=True)
103 def git_cherry_pick(revs
, commit
=False):
104 '''Cherry-picks each revision into the current branch.'''
106 return 'ERROR: No revisions selected for cherry-picking.'
108 argv
= [ 'git', 'cherry-pick' ]
109 if not commit
: argv
.append('-n')
113 output
.append('Cherry-picking: ' + rev
)
114 output
.append(run_cmd(argv
, rev
))
116 return '\n'.join(output
)
118 def git_checkout(rev
):
119 return run_cmd('git','checkout', rev
)
121 def git_commit(msg
, amend
, files
):
122 '''Creates a git commit. 'commit_all' triggers the -a
123 flag to 'git commit.' 'amend' triggers --amend.
124 'files' is a list of files to use for commits without -a.'''
126 # Sure, this is a potential "security risk," but if someone
127 # is trying to intercept/re-write commit messages on your system,
128 # then you probably have bigger problems to worry about.
129 tmpfile
= utils
.get_tmp_filename()
130 argv
= [ 'git', 'commit', '-F', tmpfile
]
132 if amend
: argv
.append('--amend')
135 return 'ERROR: No files selected for commit.'
140 # Create the commit message file
141 file = open(tmpfile
, 'w')
146 output
= run_cmd(argv
)
149 return 'Running:\t' + quote(argv
) + '\n\n' + output
151 def git_create_branch(name
, base
, track
=False):
152 '''Creates a branch starting from base. Pass track=True
153 to create a remote tracking branch.'''
154 argv
= ['git','branch']
155 if track
: argv
.append('--track')
156 return run_cmd(argv
, name
, base
)
159 def git_current_branch():
160 '''Parses 'git branch' to find the current branch.'''
161 branches
= run_cmd('git branch').splitlines()
162 for branch
in branches
:
163 if branch
.startswith('* '):
164 return branch
.lstrip('* ')
168 def git_diff(filename
, staged
=True, color
=False, with_diff_header
=False):
169 '''Invokes git_diff on filename. Passing staged=True adds
170 diffs the index against HEAD(i.e. --cached).'''
173 argv
= [ 'git', 'diff']
175 argv
.append('--color')
178 deleted
= not os
.path
.exists(filename
)
179 argv
.append('--cached')
182 argv
.append(filename
)
185 diff_lines
= diff
.splitlines()
189 del_tag
= 'deleted file mode '
192 for line
in diff_lines
:
193 if not start
and '@@ ' in line
and ' @@' in line
:
195 if start
or(deleted
and del_tag
in line
):
196 output
.write(line
+ '\n')
200 result
= output
.getvalue()
204 return(os
.linesep
.join(headers
), result
)
209 '''Returns the latest diffstat.'''
210 return run_cmd('git diff --stat HEAD^')
212 def git_format_patch(revs
, use_range
):
213 '''Exports patches revs in the 'ugit-patches' subdirectory.
214 If use_range is True, a commit range is passed to git format-patch.'''
216 argv
= ['git','format-patch','--thread','--patch-with-stat',
221 header
= 'Generated Patches:'
223 rev_range
= '%s^..%s' %( revs
[-1], revs
[0] )
225 + run_cmd(argv
, rev_range
))
229 for idx
, rev
in enumerate(revs
):
230 real_idx
= str(idx
+ num_patches
)
232 run_cmd(argv
, '-1', '--start-number', real_idx
, rev
))
234 num_patches
+= output
[-1].count('\n')
236 return '\n'.join(output
)
238 def git_config(key
, value
=None):
239 '''Gets or sets git config values. If value is not None, then
240 the config key will be set. Otherwise, the config value of the
241 config key is returned.'''
242 if value
is not None:
243 return run_cmd('git', 'config', key
, value
)
245 return run_cmd('git', 'config', '--get', key
)
247 def git_log(oneline
=True, all
=False):
248 '''Returns a pair of parallel arrays listing the revision sha1's
249 and commit summaries.'''
250 argv
= [ 'git', 'log' ]
252 argv
.append('--pretty=oneline')
257 regex
= REV_LIST_REGEX
258 output
= run_cmd(argv
)
259 for line
in output
.splitlines():
260 match
= regex
.match(line
)
262 revs
.append(match
.group(1))
263 summaries
.append(match
.group(2))
264 return( revs
, summaries
)
267 return run_cmd('git ls-files').splitlines()
269 def git_ls_tree(rev
):
270 '''Returns a list of(mode, type, sha1, path) tuples.'''
272 lines
= run_cmd('git', 'ls-tree', '-r', rev
).splitlines()
274 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
276 match
= regex
.match(line
)
278 mode
= match
.group(1)
279 objtype
= match
.group(2)
280 sha1
= match
.group(3)
281 filename
= match
.group(4)
282 output
.append((mode
, objtype
, sha1
, filename
,) )
285 def git_push(remote
, local_branch
, remote_branch
, force
=False):
286 argv
= ['git', 'push', remote
]
287 if local_branch
== remote_branch
:
288 argv
.append(local_branch
)
290 if force
and local_branch
:
291 argv
.append('+%s:%s' % ( local_branch
, remote_branch
))
293 argv
.append('%s:%s' % ( local_branch
, remote_branch
))
295 return run_cmd(argv
, with_status
=True)
297 def git_rebase(newbase
):
298 if not newbase
: return
299 return run_cmd('git','rebase', newbase
)
301 def git_remote(*args
):
302 return run_cmd('git','remote',*args
).splitlines()
304 def git_remote_show(remote
):
306 for line
in git_remote('show',remote
):
307 info
.append(line
.strip())
310 def git_remote_url(remote
):
311 return utils
.grep('^URL:\s+(.*)', git_remote_show(remote
))
313 def git_reset(to_unstage
):
314 '''Use 'git reset' to unstage files from the index.'''
316 if not to_unstage
: return 'ERROR: No files to reset.'
318 argv
= [ 'git', 'reset', '--' ]
319 argv
.extend(to_unstage
)
321 return 'Running:\t' + quote(argv
) + '\n' + run_cmd(argv
)
323 def git_rev_list_range(start
, end
):
325 argv
= [ 'git', 'rev-list', '--pretty=oneline', start
, end
]
327 raw_revs
= run_cmd(argv
).splitlines()
329 regex
= REV_LIST_REGEX
330 for line
in raw_revs
:
331 match
= regex
.match(line
)
333 rev_id
= match
.group(1)
334 summary
= match
.group(2)
335 revs
.append((rev_id
, summary
,) )
339 def git_show(sha1
, color
=False):
341 if color
: cmd
+= '--color '
342 return run_cmd(cmd
+ sha1
)
345 '''Returns a relative path to the git project root.'''
346 return run_cmd('git rev-parse --show-cdup')
349 '''RETURNS: A tuple of staged, unstaged and untracked files.
350 ( array(staged), array(unstaged), array(untracked) )'''
352 status_lines
= run_cmd('git status').splitlines()
354 unstaged_header_seen
= False
355 untracked_header_seen
= False
357 modified_header
= '# Changed but not updated:'
358 modified_regex
= re
.compile('(#\tmodified:\W{3}'
359 + '|#\tnew file:\W{3}'
360 + '|#\tdeleted:\W{4})')
362 renamed_regex
= re
.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
364 untracked_header
= '# Untracked files:'
365 untracked_regex
= re
.compile('#\t(.+)')
372 for status_line
in status_lines
:
373 if untracked_header
in status_line
:
374 untracked_header_seen
= True
376 if not untracked_header_seen
:
378 match
= untracked_regex
.match(status_line
)
380 filename
= match
.group(1)
381 untracked
.append(filename
)
383 # Staged, unstaged, and renamed files
384 for status_line
in status_lines
:
385 if modified_header
in status_line
:
386 unstaged_header_seen
= True
388 match
= modified_regex
.match(status_line
)
391 filename
= status_line
.replace(tag
, '')
392 if unstaged_header_seen
:
393 unstaged
.append(filename
)
395 staged
.append(filename
)
398 match
= renamed_regex
.match(status_line
)
400 oldname
= match
.group(2)
401 newname
= match
.group(3)
402 staged
.append(oldname
)
403 staged
.append(newname
)
405 return( staged
, unstaged
, untracked
)
408 return run_cmd('git tag').splitlines()