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 '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 '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 'No revisions selected.'
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 '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
, ffwd
=True, tags
=False):
286 argv
= ['git', 'push']
288 argv
.append('--tags')
291 if local_branch
== remote_branch
:
292 argv
.append(local_branch
)
294 if not ffwd
and local_branch
:
295 argv
.append('+%s:%s' % ( local_branch
, remote_branch
))
297 argv
.append('%s:%s' % ( local_branch
, remote_branch
))
299 return run_cmd(argv
, with_status
=True)
301 def git_rebase(newbase
):
302 if not newbase
: return
303 return run_cmd('git','rebase', newbase
)
305 def git_remote(*args
):
306 return run_cmd('git','remote',*args
).splitlines()
308 def git_remote_show(remote
):
310 for line
in git_remote('show',remote
):
311 info
.append(line
.strip())
314 def git_remote_url(remote
):
315 return utils
.grep('^URL:\s+(.*)', git_remote_show(remote
))
317 def git_reset(to_unstage
):
318 '''Use 'git reset' to unstage files from the index.'''
320 if not to_unstage
: return 'No files to reset.'
322 argv
= [ 'git', 'reset', '--' ]
323 argv
.extend(to_unstage
)
325 return 'Running:\t' + quote(argv
) + '\n' + run_cmd(argv
)
327 def git_rev_list_range(start
, end
):
329 argv
= [ 'git', 'rev-list', '--pretty=oneline', start
, end
]
331 raw_revs
= run_cmd(argv
).splitlines()
333 regex
= REV_LIST_REGEX
334 for line
in raw_revs
:
335 match
= regex
.match(line
)
337 rev_id
= match
.group(1)
338 summary
= match
.group(2)
339 revs
.append((rev_id
, summary
,) )
343 def git_show(sha1
, color
=False):
345 if color
: cmd
+= '--color '
346 return run_cmd(cmd
+ sha1
)
349 '''Returns a relative path to the git project root.'''
350 return run_cmd('git rev-parse --show-cdup')
353 '''RETURNS: A tuple of staged, unstaged and untracked files.
354 ( array(staged), array(unstaged), array(untracked) )'''
356 status_lines
= run_cmd('git status').splitlines()
358 unstaged_header_seen
= False
359 untracked_header_seen
= False
361 modified_header
= '# Changed but not updated:'
362 modified_regex
= re
.compile('(#\tmodified:\W{3}'
363 + '|#\tnew file:\W{3}'
364 + '|#\tdeleted:\W{4})')
366 renamed_regex
= re
.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
368 untracked_header
= '# Untracked files:'
369 untracked_regex
= re
.compile('#\t(.+)')
376 for status_line
in status_lines
:
377 if untracked_header
in status_line
:
378 untracked_header_seen
= True
380 if not untracked_header_seen
:
382 match
= untracked_regex
.match(status_line
)
384 filename
= match
.group(1)
385 untracked
.append(filename
)
387 # Staged, unstaged, and renamed files
388 for status_line
in status_lines
:
389 if modified_header
in status_line
:
390 unstaged_header_seen
= True
392 match
= modified_regex
.match(status_line
)
395 filename
= status_line
.replace(tag
, '')
396 if unstaged_header_seen
:
397 unstaged
.append(filename
)
399 staged
.append(filename
)
402 match
= renamed_regex
.match(status_line
)
404 oldname
= match
.group(2)
405 newname
= match
.group(3)
406 staged
.append(oldname
)
407 staged
.append(newname
)
409 return( staged
, unstaged
, untracked
)
412 return run_cmd('git tag').splitlines()