Added a "git push" dialog
[ugit.git] / ugitlibs / cmds.py
blob0614e1032d33759a13a3824d20dc491c3501323b
1 import os
2 import re
3 import commands
4 import utils
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(.*)')
12 def quote(argv):
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
17 if type(cmd) is str:
18 cmd = cmd.split(' ')
19 cmd += list(args)
20 else:
21 cmd = list(cmd + list(args))
23 child = QProcess()
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']:
38 return output
39 else:
40 if 'with_status' in kwargs:
41 return child.exitCode(), output.rstrip()
42 else:
43 return output.rstrip()
45 def git_add(to_add):
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' ]
49 argv.extend(to_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.'''
56 if not to_process:
57 return 'ERROR: No files to add or remove.'
59 to_add = []
60 output = ''
62 for filename in to_process:
63 if os.path.exists(filename):
64 to_add.append(filename)
66 if to_add:
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
72 return output
74 # Process files to add
75 argv = [ 'git', 'rm' ]
76 for filename in to_process:
77 if not os.path.exists(filename):
78 argv.append(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']
84 if indexonly:
85 argv.extend(['--index', '--cached'])
86 argv.append(filename)
87 return run_cmd(argv)
89 def git_branch(name=None, remote=False, delete=False):
90 argv = ['git', 'branch']
91 if delete and name:
92 return run_cmd(argv, '-D', name)
93 else:
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.'''
105 if not revs:
106 return 'ERROR: No revisions selected for cherry-picking.'
108 argv = [ 'git', 'cherry-pick' ]
109 if not commit: argv.append('-n')
111 output = []
112 for rev in revs:
113 output.append('Cherry-picking: ' + rev)
114 output.append(run_cmd(argv, rev))
115 output.append('')
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')
134 if not files:
135 return 'ERROR: No files selected for commit.'
137 argv.append('--')
138 argv.extend(files)
140 # Create the commit message file
141 file = open(tmpfile, 'w')
142 file.write(msg)
143 file.close()
145 # Run 'git commit'
146 output = run_cmd(argv)
147 os.unlink(tmpfile)
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('* ')
165 # Detached head?
166 return '*no branch*'
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).'''
172 deleted = False
173 argv = [ 'git', 'diff']
174 if color:
175 argv.append('--color')
177 if staged:
178 deleted = not os.path.exists(filename)
179 argv.append('--cached')
181 argv.append('--')
182 argv.append(filename)
184 diff = run_cmd(argv)
185 diff_lines = diff.splitlines()
187 output = StringIO()
188 start = False
189 del_tag = 'deleted file mode '
191 headers = []
192 for line in diff_lines:
193 if not start and '@@ ' in line and ' @@' in line:
194 start = True
195 if start or(deleted and del_tag in line):
196 output.write(line + '\n')
197 else:
198 headers.append(line)
200 result = output.getvalue()
201 output.close()
203 if with_diff_header:
204 return(os.linesep.join(headers), result)
205 else:
206 return result
208 def git_diff_stat():
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',
217 '-o','ugit-patches']
218 if len(revs) > 1:
219 argv.append('-n')
221 header = 'Generated Patches:'
222 if use_range:
223 rev_range = '%s^..%s' %( revs[-1], revs[0] )
224 return(header + '\n'
225 + run_cmd(argv, rev_range))
227 output = [ header ]
228 num_patches = 1
229 for idx, rev in enumerate(revs):
230 real_idx = str(idx + num_patches)
231 output.append(
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)
244 else:
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' ]
251 if oneline:
252 argv.append('--pretty=oneline')
253 if all:
254 argv.append('--all')
255 revs = []
256 summaries = []
257 regex = REV_LIST_REGEX
258 output = run_cmd(argv)
259 for line in output.splitlines():
260 match = regex.match(line)
261 if match:
262 revs.append(match.group(1))
263 summaries.append(match.group(2))
264 return( revs, summaries )
266 def git_ls_files():
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()
273 output = []
274 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
275 for line in lines:
276 match = regex.match(line)
277 if match:
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,) )
283 return output
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)
289 else:
290 if force and local_branch:
291 argv.append('+%s:%s' % ( local_branch, remote_branch ))
292 else:
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():
302 return run_cmd('git','remote').splitlines()
304 def git_reset(to_unstage):
305 '''Use 'git reset' to unstage files from the index.'''
307 if not to_unstage: return 'ERROR: No files to reset.'
309 argv = [ 'git', 'reset', '--' ]
310 argv.extend(to_unstage)
312 return 'Running:\t' + quote(argv) + '\n' + run_cmd(argv)
314 def git_rev_list_range(start, end):
316 argv = [ 'git', 'rev-list', '--pretty=oneline', start, end ]
318 raw_revs = run_cmd(argv).splitlines()
319 revs = []
320 regex = REV_LIST_REGEX
321 for line in raw_revs:
322 match = regex.match(line)
323 if match:
324 rev_id = match.group(1)
325 summary = match.group(2)
326 revs.append((rev_id, summary,) )
328 return revs
330 def git_show(sha1, color=False):
331 cmd = 'git show '
332 if color: cmd += '--color '
333 return run_cmd(cmd + sha1)
335 def git_show_cdup():
336 '''Returns a relative path to the git project root.'''
337 return run_cmd('git rev-parse --show-cdup')
339 def git_status():
340 '''RETURNS: A tuple of staged, unstaged and untracked files.
341 ( array(staged), array(unstaged), array(untracked) )'''
343 status_lines = run_cmd('git status').splitlines()
345 unstaged_header_seen = False
346 untracked_header_seen = False
348 modified_header = '# Changed but not updated:'
349 modified_regex = re.compile('(#\tmodified:\W{3}'
350 + '|#\tnew file:\W{3}'
351 + '|#\tdeleted:\W{4})')
353 renamed_regex = re.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
355 untracked_header = '# Untracked files:'
356 untracked_regex = re.compile('#\t(.+)')
358 staged = []
359 unstaged = []
360 untracked = []
362 # Untracked files
363 for status_line in status_lines:
364 if untracked_header in status_line:
365 untracked_header_seen = True
366 continue
367 if not untracked_header_seen:
368 continue
369 match = untracked_regex.match(status_line)
370 if match:
371 filename = match.group(1)
372 untracked.append(filename)
374 # Staged, unstaged, and renamed files
375 for status_line in status_lines:
376 if modified_header in status_line:
377 unstaged_header_seen = True
378 continue
379 match = modified_regex.match(status_line)
380 if match:
381 tag = match.group(0)
382 filename = status_line.replace(tag, '')
383 if unstaged_header_seen:
384 unstaged.append(filename)
385 else:
386 staged.append(filename)
387 continue
388 # Renamed files
389 match = renamed_regex.match(status_line)
390 if match:
391 oldname = match.group(2)
392 newname = match.group(3)
393 staged.append(oldname)
394 staged.append(newname)
396 return( staged, unstaged, untracked )
398 def git_tag():
399 return run_cmd('git tag').splitlines()