Added a push dialog and a some UI niceties
[ugit.git] / ugitlibs / cmds.py
blobd08fcd09214f92158bb59c73c688d0a102e8f3a6
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(*args):
302 return run_cmd('git','remote',*args).splitlines()
304 def git_remote_show(remote):
305 info = []
306 for line in git_remote('show',remote):
307 info.append(line.strip())
308 return info
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()
328 revs = []
329 regex = REV_LIST_REGEX
330 for line in raw_revs:
331 match = regex.match(line)
332 if match:
333 rev_id = match.group(1)
334 summary = match.group(2)
335 revs.append((rev_id, summary,) )
337 return revs
339 def git_show(sha1, color=False):
340 cmd = 'git show '
341 if color: cmd += '--color '
342 return run_cmd(cmd + sha1)
344 def git_show_cdup():
345 '''Returns a relative path to the git project root.'''
346 return run_cmd('git rev-parse --show-cdup')
348 def git_status():
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(.+)')
367 staged = []
368 unstaged = []
369 untracked = []
371 # Untracked files
372 for status_line in status_lines:
373 if untracked_header in status_line:
374 untracked_header_seen = True
375 continue
376 if not untracked_header_seen:
377 continue
378 match = untracked_regex.match(status_line)
379 if match:
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
387 continue
388 match = modified_regex.match(status_line)
389 if match:
390 tag = match.group(0)
391 filename = status_line.replace(tag, '')
392 if unstaged_header_seen:
393 unstaged.append(filename)
394 else:
395 staged.append(filename)
396 continue
397 # Renamed files
398 match = renamed_regex.match(status_line)
399 if match:
400 oldname = match.group(2)
401 newname = match.group(3)
402 staged.append(oldname)
403 staged.append(newname)
405 return( staged, unstaged, untracked )
407 def git_tag():
408 return run_cmd('git tag').splitlines()