Exclude a line from inclusion in a diff if the selection only contains its trailing...
[ugit.git] / ugitlibs / cmds.py
blob9f6c05c181f4ed0172b82aa6cd776c632e8b21ac
1 '''TODO: "import stgit"'''
2 import os
3 import re
4 import types
5 import commands
6 import utils
7 from cStringIO import StringIO
9 from PyQt4.QtCore import QProcess
10 from PyQt4.QtCore import QObject
11 import PyQt4.QtGui
13 # A regex for matching the output of git(log|rev-list) --pretty=oneline
14 REV_LIST_REGEX = re.compile('([0-9a-f]+)\W(.*)')
16 def quote(argv):
17 return ' '.join([ utils.shell_quote(arg) for arg in argv ])
19 def git(*args,**kwargs):
20 return run_cmd('git', *args, **kwargs)
22 def run_cmd(cmd, *args, **kwargs):
23 # Handle cmd as either a string or an argv list
24 if type(cmd) is str:
25 cmd = cmd.split(' ')
26 cmd += list(args)
27 else:
28 cmd = list(cmd + list(args))
30 child = QProcess()
31 child.setProcessChannelMode(QProcess.MergedChannels);
32 child.start(cmd[0], cmd[1:])
34 if(not child.waitForStarted()):
35 raise Exception("failed to start child")
37 if(not child.waitForFinished()):
38 raise Exception("failed to start child")
40 output = str(child.readAll())
42 # Allow run_cmd(argv, raw=True) for when we
43 # want the full, raw output(e.g. git cat-file)
44 if 'raw' in kwargs and kwargs['raw']:
45 return output
46 else:
47 if 'with_status' in kwargs:
48 return child.exitCode(), output.rstrip()
49 else:
50 return output.rstrip()
52 def git_add(to_add):
53 '''Invokes 'git add' to index the filenames in to_add.'''
54 if not to_add: return 'No files to add.'
55 argv = [ 'git', 'add' ]
56 argv.extend(to_add)
57 return quote(argv) + '\n' + run_cmd(argv)
59 def git_add_or_remove(to_process):
60 '''Invokes 'git add' to index the filenames in to_process that exist
61 and 'git rm' for those that do not exist.'''
63 if not to_process:
64 return 'No files to add or remove.'
66 to_add = []
67 output = ''
69 for filename in to_process:
70 if os.path.exists(filename):
71 to_add.append(filename)
73 git_add(to_add)
75 if len(to_add) == len(to_process):
76 # to_process only contained unremoved files --
77 # short-circuit the removal checks
78 return None
80 # Process files to remote
81 argv = [ 'git', 'rm' ]
82 for filename in to_process:
83 if not os.path.exists(filename):
84 argv.append(filename)
86 run_cmd(argv)
87 return None
89 def git_apply(filename, indexonly=True, reverse=False):
90 argv = ['git', 'apply']
91 if reverse: argv.append('--reverse')
92 if indexonly: argv.extend(['--index', '--cached'])
93 argv.append(filename)
94 return run_cmd(argv)
96 def git_branch(name=None, remote=False, delete=False):
97 argv = ['git', 'branch']
98 if delete and name:
99 return run_cmd(argv, '-D', name)
100 else:
101 if remote: argv.append('-r')
103 branches = run_cmd(argv).splitlines()
104 return map(lambda(x): x.lstrip('* '), branches)
106 def git_cat_file(objtype, sha1):
107 cmd = 'git cat-file %s %s' %( objtype, sha1 )
108 return run_cmd(cmd, raw=True)
110 def git_cherry_pick(revs, commit=False):
111 '''Cherry-picks each revision into the current branch.'''
112 if not revs:
113 return 'No revision selected.'
115 argv = [ 'git', 'cherry-pick' ]
116 if not commit: argv.append('-n')
118 output = []
119 for rev in revs:
120 output.append('Cherry picking:' + ' '+ rev)
121 output.append(run_cmd(argv, rev))
122 output.append('')
123 return '\n'.join(output)
125 def git_checkout(rev):
126 return run_cmd('git','checkout', rev)
128 def git_commit(msg, amend=False):
129 '''Creates a git commit. 'commit_all' triggers the -a
130 flag to 'git commit.' 'amend' triggers --amend.
131 'files' is a list of files to use for commits without -a.'''
133 # Sure, this is a potential "security risk," but if someone
134 # is trying to intercept/re-write commit messages on your system,
135 # then you probably have bigger problems to worry about.
136 tmpfile = utils.get_tmp_filename()
137 argv = [ 'git', 'commit', '-F', tmpfile ]
138 if amend: argv.append('--amend')
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 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 'Detached HEAD'
168 def git_diff(commit=None,filename=None, color=False,
169 cached=True, with_diff_header=False,
170 reverse=False):
171 "Invokes git_diff on a filepath."
173 argv = [ 'diff']
174 if reverse: argv.append('-R')
175 if color: argv.append('--color')
176 if cached: argv.append('--cached')
178 deleted = cached and not os.path.exists(filename)
180 if filename:
181 argv.append('--')
182 argv.append(filename)
184 if commit:
185 argv.append('%s^..%s' % (commit,commit))
187 diff = git(*argv)
188 diff_lines = diff.splitlines()
190 output = StringIO()
191 start = False
192 del_tag = 'deleted file mode '
194 headers = []
195 for line in diff_lines:
196 if not start and '@@ ' in line and ' @@' in line:
197 start = True
198 if start or(deleted and del_tag in line):
199 output.write(line + '\n')
200 else:
201 headers.append(line)
203 result = output.getvalue()
204 output.close()
206 if with_diff_header:
207 return(os.linesep.join(headers), result)
208 else:
209 return result
211 def git_diff_stat():
212 '''Returns the latest diffstat.'''
213 return run_cmd('git diff --stat HEAD^')
215 def git_format_patch(revs, use_range):
216 '''Exports patches revs in the 'ugit-patches' subdirectory.
217 If use_range is True, a commit range is passed to git format-patch.'''
219 argv = ['git','format-patch','--thread','--patch-with-stat',
220 '-o','ugit-patches']
221 if len(revs) > 1:
222 argv.append('-n')
224 header = 'Generated Patches:'
225 if use_range:
226 rev_range = '%s^..%s' %( revs[-1], revs[0] )
227 return(header + '\n'
228 + run_cmd(argv, rev_range))
230 output = [ header ]
231 num_patches = 1
232 for idx, rev in enumerate(revs):
233 real_idx = str(idx + num_patches)
234 output.append(
235 run_cmd(argv, '-1', '--start-number', real_idx, rev))
237 num_patches += output[-1].count('\n')
239 return '\n'.join(output)
241 def git_config(key, value=None):
242 '''Gets or sets git config values. If value is not None, then
243 the config key will be set. Otherwise, the config value of the
244 config key is returned.'''
245 if value is not None:
246 return run_cmd('git', 'config', key, value)
247 else:
248 return run_cmd('git', 'config', '--get', key)
250 def git_log(oneline=True, all=False):
251 '''Returns a pair of parallel arrays listing the revision sha1's
252 and commit summaries.'''
253 argv = [ 'git', 'log' ]
254 if oneline:
255 argv.append('--pretty=oneline')
256 if all:
257 argv.append('--all')
258 revs = []
259 summaries = []
260 regex = REV_LIST_REGEX
261 output = run_cmd(argv)
262 for line in output.splitlines():
263 match = regex.match(line)
264 if match:
265 revs.append(match.group(1))
266 summaries.append(match.group(2))
267 return( revs, summaries )
269 def git_ls_files():
270 return run_cmd('git ls-files').splitlines()
272 def git_ls_tree(rev):
273 '''Returns a list of(mode, type, sha1, path) tuples.'''
275 lines = run_cmd('git', 'ls-tree', '-r', rev).splitlines()
276 output = []
277 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
278 for line in lines:
279 match = regex.match(line)
280 if match:
281 mode = match.group(1)
282 objtype = match.group(2)
283 sha1 = match.group(3)
284 filename = match.group(4)
285 output.append((mode, objtype, sha1, filename,) )
286 return output
288 def git_push(remote, local_branch, remote_branch, ffwd=True, tags=False):
289 argv = ['git', 'push']
290 if tags:
291 argv.append('--tags')
292 argv.append(remote)
294 if local_branch == remote_branch:
295 argv.append(local_branch)
296 else:
297 if not ffwd and local_branch:
298 argv.append('+%s:%s' % ( local_branch, remote_branch ))
299 else:
300 argv.append('%s:%s' % ( local_branch, remote_branch ))
302 return run_cmd(argv, with_status=True)
304 def git_rebase(newbase):
305 if not newbase: return
306 return run_cmd('git','rebase', newbase)
308 def git_remote(*args):
309 return run_cmd('git','remote',*args).splitlines()
311 def git_remote_show(remote):
312 info = []
313 for line in git_remote('show',remote):
314 info.append(line.strip())
315 return info
317 def git_remote_url(remote):
318 return utils.grep('^URL:\s+(.*)', git_remote_show(remote))
320 def git_reset(to_unstage):
321 '''Use 'git reset' to unstage files from the index.'''
323 if not to_unstage:
324 return 'No files to reset.'
326 argv = [ 'git', 'reset', '--' ]
327 argv.extend(to_unstage)
329 return quote(argv) + '\n' + run_cmd(argv)
331 def git_rev_list_range(start, end):
333 argv = [ 'git', 'rev-list', '--pretty=oneline', start, end ]
335 raw_revs = run_cmd(argv).splitlines()
336 revs = []
337 regex = REV_LIST_REGEX
338 for line in raw_revs:
339 match = regex.match(line)
340 if match:
341 rev_id = match.group(1)
342 summary = match.group(2)
343 revs.append((rev_id, summary,) )
345 return revs
347 def git_show(sha1, color=False):
348 cmd = 'git show '
349 if color: cmd += '--color '
350 return run_cmd(cmd + sha1)
352 def git_show_cdup():
353 '''Returns a relative path to the git project root.'''
354 return run_cmd('git rev-parse --show-cdup')
356 def git_status():
357 '''RETURNS: A tuple of staged, unstaged and untracked files.
358 ( array(staged), array(unstaged), array(untracked) )'''
360 status_lines = run_cmd('git status').splitlines()
362 unstaged_header_seen = False
363 untracked_header_seen = False
365 modified_header = '# Changed but not updated:'
366 modified_regex = re.compile('(#\tmodified:\W{3}'
367 + '|#\tnew file:\W{3}'
368 + '|#\tdeleted:\W{4})')
370 renamed_regex = re.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
372 untracked_header = '# Untracked files:'
373 untracked_regex = re.compile('#\t(.+)')
375 staged = []
376 unstaged = []
377 untracked = []
379 # Untracked files
380 for status_line in status_lines:
381 if untracked_header in status_line:
382 untracked_header_seen = True
383 continue
384 if not untracked_header_seen:
385 continue
386 match = untracked_regex.match(status_line)
387 if match:
388 filename = match.group(1)
389 untracked.append(filename)
391 # Staged, unstaged, and renamed files
392 for status_line in status_lines:
393 if modified_header in status_line:
394 unstaged_header_seen = True
395 continue
396 match = modified_regex.match(status_line)
397 if match:
398 tag = match.group(0)
399 filename = status_line.replace(tag, '')
400 if unstaged_header_seen:
401 unstaged.append(filename)
402 else:
403 staged.append(filename)
404 continue
405 # Renamed files
406 match = renamed_regex.match(status_line)
407 if match:
408 oldname = match.group(2)
409 newname = match.group(3)
410 staged.append(oldname)
411 staged.append(newname)
413 return( staged, unstaged, untracked )
415 def git_tag():
416 return run_cmd('git tag').splitlines()