Updated TODO
[ugit.git] / ugitlibs / cmds.py
blob21c4811e564c5a09029d2c32ecb7d67cdd7f5a8a
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 '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 '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 'No revisions selected.'
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 '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, ffwd=True, tags=False):
286 argv = ['git', 'push']
287 if tags:
288 argv.append('--tags')
289 argv.append(remote)
291 if local_branch == remote_branch:
292 argv.append(local_branch)
293 else:
294 if not ffwd and local_branch:
295 argv.append('+%s:%s' % ( local_branch, remote_branch ))
296 else:
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):
309 info = []
310 for line in git_remote('show',remote):
311 info.append(line.strip())
312 return info
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()
332 revs = []
333 regex = REV_LIST_REGEX
334 for line in raw_revs:
335 match = regex.match(line)
336 if match:
337 rev_id = match.group(1)
338 summary = match.group(2)
339 revs.append((rev_id, summary,) )
341 return revs
343 def git_show(sha1, color=False):
344 cmd = 'git show '
345 if color: cmd += '--color '
346 return run_cmd(cmd + sha1)
348 def git_show_cdup():
349 '''Returns a relative path to the git project root.'''
350 return run_cmd('git rev-parse --show-cdup')
352 def git_status():
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(.+)')
371 staged = []
372 unstaged = []
373 untracked = []
375 # Untracked files
376 for status_line in status_lines:
377 if untracked_header in status_line:
378 untracked_header_seen = True
379 continue
380 if not untracked_header_seen:
381 continue
382 match = untracked_regex.match(status_line)
383 if match:
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
391 continue
392 match = modified_regex.match(status_line)
393 if match:
394 tag = match.group(0)
395 filename = status_line.replace(tag, '')
396 if unstaged_header_seen:
397 unstaged.append(filename)
398 else:
399 staged.append(filename)
400 continue
401 # Renamed files
402 match = renamed_regex.match(status_line)
403 if match:
404 oldname = match.group(2)
405 newname = match.group(3)
406 staged.append(oldname)
407 staged.append(newname)
409 return( staged, unstaged, untracked )
411 def git_tag():
412 return run_cmd('git tag').splitlines()