A few more i18n fixes
[ugit.git] / ugitlibs / cmds.py
blob5c6a75dc6a3e0207db80c98369a0fef72f015dee
1 import os
2 import re
3 import commands
4 import utils
5 from cStringIO import StringIO
7 from PyQt4.QtCore import QProcess
8 from PyQt4.QtCore import QObject
9 import PyQt4.QtGui
11 # A regex for matching the output of git(log|rev-list) --pretty=oneline
12 REV_LIST_REGEX = re.compile('([0-9a-f]+)\W(.*)')
14 def quote(argv):
15 return ' '.join([ utils.shell_quote(arg) for arg in argv ])
17 def run_cmd(cmd, *args, **kwargs):
18 # Handle cmd as either a string or an argv list
19 if type(cmd) is str:
20 cmd = cmd.split(' ')
21 cmd += list(args)
22 else:
23 cmd = list(cmd + list(args))
25 child = QProcess()
26 child.setProcessChannelMode(QProcess.MergedChannels);
27 child.start(cmd[0], cmd[1:])
29 if(not child.waitForStarted()):
30 raise Exception("failed to start child")
32 if(not child.waitForFinished()):
33 raise Exception("failed to start child")
35 output = str(child.readAll())
37 # Allow run_cmd(argv, raw=True) for when we
38 # want the full, raw output(e.g. git cat-file)
39 if 'raw' in kwargs and kwargs['raw']:
40 return output
41 else:
42 if 'with_status' in kwargs:
43 return child.exitCode(), output.rstrip()
44 else:
45 return output.rstrip()
47 def git_add(to_add):
48 '''Invokes 'git add' to index the filenames in to_add.'''
49 if not to_add: return 'No files to add.'
50 argv = [ 'git', 'add' ]
51 argv.extend(to_add)
52 return quote(argv) + '\n' + run_cmd(argv)
54 def git_add_or_remove(to_process):
55 '''Invokes 'git add' to index the filenames in to_process that exist
56 and 'git rm' for those that do not exist.'''
58 if not to_process:
59 return 'No files to add or remove.'
61 to_add = []
62 output = ''
64 for filename in to_process:
65 if os.path.exists(filename):
66 to_add.append(filename)
68 git_add(to_add)
70 if len(to_add) == len(to_process):
71 # to_process only contained unremoved files --
72 # short-circuit the removal checks
73 return None
75 # Process files to remote
76 argv = [ 'git', 'rm' ]
77 for filename in to_process:
78 if not os.path.exists(filename):
79 argv.append(filename)
81 run_cmd(argv)
82 return None
84 def git_apply(filename, indexonly=True):
85 argv = ['git', 'apply']
86 if indexonly:
87 argv.extend(['--index', '--cached'])
88 argv.append(filename)
89 return run_cmd(argv)
91 def git_branch(name=None, remote=False, delete=False):
92 argv = ['git', 'branch']
93 if delete and name:
94 return run_cmd(argv, '-D', name)
95 else:
96 if remote: argv.append('-r')
98 branches = run_cmd(argv).splitlines()
99 return map(lambda(x): x.lstrip('* '), branches)
101 def git_cat_file(objtype, sha1):
102 cmd = 'git cat-file %s %s' %( objtype, sha1 )
103 return run_cmd(cmd, raw=True)
105 def git_cherry_pick(revs, commit=False):
106 '''Cherry-picks each revision into the current branch.'''
107 if not revs:
108 return 'No revision selected.'
110 argv = [ 'git', 'cherry-pick' ]
111 if not commit: argv.append('-n')
113 output = []
114 for rev in revs:
115 output.append('Cherry picking:' + ' '+ rev)
116 output.append(run_cmd(argv, rev))
117 output.append('')
118 return '\n'.join(output)
120 def git_checkout(rev):
121 return run_cmd('git','checkout', rev)
123 def git_commit(msg, amend, files):
124 '''Creates a git commit. 'commit_all' triggers the -a
125 flag to 'git commit.' 'amend' triggers --amend.
126 'files' is a list of files to use for commits without -a.'''
128 # Sure, this is a potential "security risk," but if someone
129 # is trying to intercept/re-write commit messages on your system,
130 # then you probably have bigger problems to worry about.
131 tmpfile = utils.get_tmp_filename()
132 argv = [ 'git', 'commit', '-F', tmpfile ]
134 if amend: argv.append('--amend')
136 if not files:
137 return 'No files selected.'
139 argv.append('--')
140 argv.extend(files)
142 # Create the commit message file
143 file = open(tmpfile, 'w')
144 file.write(msg)
145 file.close()
147 # Run 'git commit'
148 output = run_cmd(argv)
149 os.unlink(tmpfile)
151 return quote(argv) + '\n\n' + output
153 def git_create_branch(name, base, track=False):
154 '''Creates a branch starting from base. Pass track=True
155 to create a remote tracking branch.'''
156 argv = ['git','branch']
157 if track: argv.append('--track')
158 return run_cmd(argv, name, base)
161 def git_current_branch():
162 '''Parses 'git branch' to find the current branch.'''
163 branches = run_cmd('git branch').splitlines()
164 for branch in branches:
165 if branch.startswith('* '):
166 return branch.lstrip('* ')
167 # Detached head?
168 return 'Detached HEAD'
170 def git_diff(filename, staged=True, color=False, with_diff_header=False):
171 '''Invokes git_diff on filename. Passing staged=True adds
172 diffs the index against HEAD(i.e. --cached).'''
174 deleted = False
175 argv = [ 'git', 'diff']
176 if color:
177 argv.append('--color')
179 if staged:
180 deleted = not os.path.exists(filename)
181 argv.append('--cached')
183 argv.append('--')
184 argv.append(filename)
186 diff = run_cmd(argv)
187 diff_lines = diff.splitlines()
189 output = StringIO()
190 start = False
191 del_tag = 'deleted file mode '
193 headers = []
194 for line in diff_lines:
195 if not start and '@@ ' in line and ' @@' in line:
196 start = True
197 if start or(deleted and del_tag in line):
198 output.write(line + '\n')
199 else:
200 headers.append(line)
202 result = output.getvalue()
203 output.close()
205 if with_diff_header:
206 return(os.linesep.join(headers), result)
207 else:
208 return result
210 def git_diff_stat():
211 '''Returns the latest diffstat.'''
212 return run_cmd('git diff --stat HEAD^')
214 def git_format_patch(revs, use_range):
215 '''Exports patches revs in the 'ugit-patches' subdirectory.
216 If use_range is True, a commit range is passed to git format-patch.'''
218 argv = ['git','format-patch','--thread','--patch-with-stat',
219 '-o','ugit-patches']
220 if len(revs) > 1:
221 argv.append('-n')
223 header = 'Generated Patches:'
224 if use_range:
225 rev_range = '%s^..%s' %( revs[-1], revs[0] )
226 return(header + '\n'
227 + run_cmd(argv, rev_range))
229 output = [ header ]
230 num_patches = 1
231 for idx, rev in enumerate(revs):
232 real_idx = str(idx + num_patches)
233 output.append(
234 run_cmd(argv, '-1', '--start-number', real_idx, rev))
236 num_patches += output[-1].count('\n')
238 return '\n'.join(output)
240 def git_config(key, value=None):
241 '''Gets or sets git config values. If value is not None, then
242 the config key will be set. Otherwise, the config value of the
243 config key is returned.'''
244 if value is not None:
245 return run_cmd('git', 'config', key, value)
246 else:
247 return run_cmd('git', 'config', '--get', key)
249 def git_log(oneline=True, all=False):
250 '''Returns a pair of parallel arrays listing the revision sha1's
251 and commit summaries.'''
252 argv = [ 'git', 'log' ]
253 if oneline:
254 argv.append('--pretty=oneline')
255 if all:
256 argv.append('--all')
257 revs = []
258 summaries = []
259 regex = REV_LIST_REGEX
260 output = run_cmd(argv)
261 for line in output.splitlines():
262 match = regex.match(line)
263 if match:
264 revs.append(match.group(1))
265 summaries.append(match.group(2))
266 return( revs, summaries )
268 def git_ls_files():
269 return run_cmd('git ls-files').splitlines()
271 def git_ls_tree(rev):
272 '''Returns a list of(mode, type, sha1, path) tuples.'''
274 lines = run_cmd('git', 'ls-tree', '-r', rev).splitlines()
275 output = []
276 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
277 for line in lines:
278 match = regex.match(line)
279 if match:
280 mode = match.group(1)
281 objtype = match.group(2)
282 sha1 = match.group(3)
283 filename = match.group(4)
284 output.append((mode, objtype, sha1, filename,) )
285 return output
287 def git_push(remote, local_branch, remote_branch, ffwd=True, tags=False):
288 argv = ['git', 'push']
289 if tags:
290 argv.append('--tags')
291 argv.append(remote)
293 if local_branch == remote_branch:
294 argv.append(local_branch)
295 else:
296 if not ffwd and local_branch:
297 argv.append('+%s:%s' % ( local_branch, remote_branch ))
298 else:
299 argv.append('%s:%s' % ( local_branch, remote_branch ))
301 return run_cmd(argv, with_status=True)
303 def git_rebase(newbase):
304 if not newbase: return
305 return run_cmd('git','rebase', newbase)
307 def git_remote(*args):
308 return run_cmd('git','remote',*args).splitlines()
310 def git_remote_show(remote):
311 info = []
312 for line in git_remote('show',remote):
313 info.append(line.strip())
314 return info
316 def git_remote_url(remote):
317 return utils.grep('^URL:\s+(.*)', git_remote_show(remote))
319 def git_reset(to_unstage):
320 '''Use 'git reset' to unstage files from the index.'''
322 if not to_unstage:
323 return 'No files to reset.'
325 argv = [ 'git', 'reset', '--' ]
326 argv.extend(to_unstage)
328 return quote(argv) + '\n' + run_cmd(argv)
330 def git_rev_list_range(start, end):
332 argv = [ 'git', 'rev-list', '--pretty=oneline', start, end ]
334 raw_revs = run_cmd(argv).splitlines()
335 revs = []
336 regex = REV_LIST_REGEX
337 for line in raw_revs:
338 match = regex.match(line)
339 if match:
340 rev_id = match.group(1)
341 summary = match.group(2)
342 revs.append((rev_id, summary,) )
344 return revs
346 def git_show(sha1, color=False):
347 cmd = 'git show '
348 if color: cmd += '--color '
349 return run_cmd(cmd + sha1)
351 def git_show_cdup():
352 '''Returns a relative path to the git project root.'''
353 return run_cmd('git rev-parse --show-cdup')
355 def git_status():
356 '''RETURNS: A tuple of staged, unstaged and untracked files.
357 ( array(staged), array(unstaged), array(untracked) )'''
359 status_lines = run_cmd('git status').splitlines()
361 unstaged_header_seen = False
362 untracked_header_seen = False
364 modified_header = '# Changed but not updated:'
365 modified_regex = re.compile('(#\tmodified:\W{3}'
366 + '|#\tnew file:\W{3}'
367 + '|#\tdeleted:\W{4})')
369 renamed_regex = re.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
371 untracked_header = '# Untracked files:'
372 untracked_regex = re.compile('#\t(.+)')
374 staged = []
375 unstaged = []
376 untracked = []
378 # Untracked files
379 for status_line in status_lines:
380 if untracked_header in status_line:
381 untracked_header_seen = True
382 continue
383 if not untracked_header_seen:
384 continue
385 match = untracked_regex.match(status_line)
386 if match:
387 filename = match.group(1)
388 untracked.append(filename)
390 # Staged, unstaged, and renamed files
391 for status_line in status_lines:
392 if modified_header in status_line:
393 unstaged_header_seen = True
394 continue
395 match = modified_regex.match(status_line)
396 if match:
397 tag = match.group(0)
398 filename = status_line.replace(tag, '')
399 if unstaged_header_seen:
400 unstaged.append(filename)
401 else:
402 staged.append(filename)
403 continue
404 # Renamed files
405 match = renamed_regex.match(status_line)
406 if match:
407 oldname = match.group(2)
408 newname = match.group(3)
409 staged.append(oldname)
410 staged.append(newname)
412 return( staged, unstaged, untracked )
414 def git_tag():
415 return run_cmd('git tag').splitlines()