Merged branchmenu into master
[ugit.git] / py / cmds.py
blob5abf930b7e7116cecbbaf46915110a760c63eea6
1 import os
2 import re
3 import time
4 import commands
5 import utils
7 # A regex for matching the output of git (log|rev-list) --pretty=oneline
8 REV_LIST_PATTERN = '([0-9a-f]+)\W(.*)'
10 def git_add (to_add):
11 '''Invokes 'git add' to index the filenames in to_add.'''
13 if not to_add: return 'ERROR: No files to add.'
15 argv = [ 'git', 'add' ]
16 for filename in to_add:
17 argv.append (utils.shell_quote (filename))
19 cmd = ' '.join (argv)
20 return 'Running:\t%s\n%s\n%s added successfully' % (
21 cmd, commands.getoutput (cmd), ', '.join (to_add) )
23 def git_add_or_remove (to_process):
24 '''Invokes 'git add' to index the filenames in to_process that exist
25 and 'git rm' for those that do not exist.'''
27 if not to_process: return 'ERROR: No files to add or remove.'
29 to_add = []
30 output = ''
32 for filename in to_process:
33 if os.path.exists (filename):
34 to_add.append (filename)
36 if to_add:
37 output += git_add (to_add) + '\n\n'
39 if len(to_add) == len(to_process):
40 # to_process only contained unremoved files --
41 # short-circuit the removal checks
42 return output
44 # Process files to add
45 argv = [ 'git', 'rm' ]
46 for filename in to_process:
47 if not os.path.exists (filename):
48 argv.append (utils.shell_quote (filename))
50 cmd = ' '.join (argv)
51 return output + 'Running: %s\n%s' % ( cmd, commands.getoutput (cmd) )
53 def git_branch (remote=False):
54 '''Returns a list of git branches.
55 Pass "remote=True" to list remote branches.'''
56 cmd = 'git branch'
57 if remote: cmd += ' -r'
58 branches = commands.getoutput (cmd).split ('\n')
59 return map (lambda (x): x.lstrip ('* '), branches)
61 def git_cat_file (objtype, sha1, target_file=None):
62 cmd = 'git cat-file %s %s' % ( objtype, sha1 )
63 if target_file:
64 cmd += '> %s' % utils.shell_quote (target_file)
65 return commands.getoutput (cmd)
67 def git_cherry_pick (revs, commit=False):
68 '''Cherry-picks each revision into the current branch.'''
69 if not revs:
70 return 'ERROR: No revisions selected for cherry-picking.'''
72 cmd = 'git cherry-pick '
73 if not commit: cmd += '-n '
74 output = []
75 for rev in revs:
76 output.append ('Cherry-picking: ' + rev)
77 output.append (commands.getoutput (cmd + rev))
78 output.append ('')
79 return '\n'.join (output)
81 def git_commit (msg, amend, files):
82 '''Creates a git commit. 'commit_all' triggers the -a
83 flag to 'git commit.' 'amend' triggers --amend.
84 'files' is a list of files to use for commits without -a.'''
86 # Allow TMPDIR/TMP with a fallback to /tmp
87 tmpdir = os.getenv ('TMPDIR', os.getenv ('TMP', '/tmp'))
89 # Sure, this is a potential "security risk," but if someone
90 # is trying to intercept/re-write commit messages on your system,
91 # then you probably have bigger problems to worry about.
92 tmpfile = os.path.join (tmpdir,
93 'ugit.%s.%s' % ( os.getuid(), time.time() ))
95 argv = [ 'git', 'commit', '-F', tmpfile ]
97 if amend: argv.append ('--amend')
99 if not files:
100 return 'ERROR: No files selected for commit.'
102 argv.append ('--')
103 for file in files:
104 argv.append (utils.shell_quote (file))
106 # Create the commit message file
107 file = open (tmpfile, 'w')
108 file.write (msg)
109 file.close()
111 # Run 'git commit'
112 cmd = ' '.join (argv)
113 output = commands.getoutput (cmd)
114 os.unlink (tmpfile)
116 return 'Running:\t%s\n%s' % (cmd, output)
118 def git_create_branch (name, base, track=False):
119 '''Creates a branch starting from base. Pass track=True
120 to create a remote tracking branch.'''
121 cmd = 'git branch'
122 if track: cmd += ' --track '
123 cmd += '%s %s' % ( utils.shell_quote (name),
124 utils.shell_quote (base))
125 return commands.getoutput (cmd)
128 def git_current_branch():
129 '''Parses 'git branch' to find the current branch.'''
130 branches = commands.getoutput ('git branch').split ('\n')
131 for branch in branches:
132 if branch.startswith ('* '):
133 return branch.lstrip ('* ')
134 raise Exception, 'No current branch. Detached HEAD?'
136 def git_diff (filename, staged=True):
137 '''Invokes git_diff on filename. Passing staged=True adds
138 diffs the index against HEAD (i.e. --cached).'''
140 deleted = False
141 argv = [ 'git', 'diff', '--color']
142 if staged:
143 deleted = not os.path.exists (filename)
144 argv.append ('--cached')
146 argv.append ('--')
147 argv.append (utils.shell_quote (filename))
149 diff = commands.getoutput (' '.join (argv))
150 diff_lines = diff.split ('\n')
152 output = []
153 start = False
154 del_tag = 'deleted file mode '
156 for line in diff_lines:
157 if not start and '@@ ' in line and ' @@' in line:
158 start = True
159 if start or (deleted and del_tag in line):
160 output.append (line)
161 return '\n'.join (output)
163 def git_diff_stat ():
164 '''Returns the latest diffstat.'''
165 return commands.getoutput ('git diff --color --stat HEAD^')
167 def git_format_patch (revs, use_range):
168 '''Exports patches revs in the 'ugit-patches' subdirectory.
169 If use_range is True, a commit range is passed to git format-patch.'''
171 cmd = 'git format-patch --thread --patch-with-stat -o ugit-patches '
172 header = 'Generated Patches:'
173 if len (revs) > 1:
174 cmd += '-n '
176 if use_range:
177 rev_range = '%s^..%s' % ( revs[-1], revs[0] )
178 return header + '\n' + commands.getoutput (cmd + rev_range)
180 output = [ header ]
181 num_patches = 1
182 for idx, rev in enumerate (revs):
183 real_idx = idx + num_patches
184 revcmd = cmd + '-1 --start-number %d %s' % (real_idx, rev)
185 output.append (commands.getoutput (revcmd))
186 num_patches += output[-1].count ('\n')
187 return '\n'.join (output)
189 def git_config(key, value=None):
190 '''Gets or sets git config values. If value is not None, then
191 the config key will be set. Otherwise, the config value of the
192 config key is returned.'''
193 k = utils.shell_quote (key)
194 if value is not None:
195 v = utils.shell_quote (value)
196 return commands.getoutput ('git config --set %s %s' % (k, v))
197 else:
198 return commands.getoutput ('git config --get %s' % k)
200 def git_log (oneline=True, all=False):
201 '''Returns a pair of parallel arrays listing the revision sha1's
202 and commit summaries.'''
203 argv = [ 'git', 'log' ]
204 if oneline: argv.append ('--pretty=oneline')
205 if all: argv.append ('--all')
206 revs = []
207 summaries = []
208 regex = re.compile (REV_LIST_PATTERN)
209 output = commands.getoutput (' '.join (argv))
210 for line in output.split ('\n'):
211 match = regex.match (line)
212 if match:
213 revs.append (match.group (1))
214 summaries.append (match.group (2))
215 return ( revs, summaries )
217 def git_ls_tree (rev):
218 '''Returns a list of (mode, type, sha1, path) tuples.'''
219 regex = re.compile ('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
220 sh_rev = utils.shell_quote (rev)
221 lines = commands.getoutput ('git ls-tree -r ' + sh_rev).split ('\n')
222 output = []
223 for line in lines:
224 match = regex.match (line)
225 if match:
226 mode = match.group (1)
227 objtype = match.group (2)
228 sha1 = match.group (3)
229 filename = match.group (4)
230 output.append ( (mode, objtype, sha1, filename,) )
231 return output
233 def git_reset (to_unstage):
234 '''Use 'git reset' to unstage files from the index.'''
236 if not to_unstage: return 'ERROR: No files to reset.'
238 argv = [ 'git', 'reset', '--' ]
239 for filename in to_unstage:
240 argv.append (utils.shell_quote (filename))
242 cmd = ' '.join (argv)
243 return 'Running:\t%s\n%s' % ( cmd, commands.getoutput (cmd) )
245 def git_rev_list_range (rev_start, rev_end):
246 cmd = ('git rev-list --pretty=oneline %s..%s'
247 % (utils.shell_quote (rev_start), utils.shell_quote(rev_end)))
249 revs = []
250 raw_revs = commands.getoutput (cmd).split ('\n')
251 regex = re.compile (REV_LIST_PATTERN)
252 for line in raw_revs:
253 match = regex.match (line)
254 if match:
255 rev_id = match.group (1)
256 summary = match.group (2)
257 revs.append ( (rev_id, summary,) )
259 return revs
261 def git_show (sha1, color=False):
262 cmd = 'git show '
263 if color: cmd += '--color '
264 return commands.getoutput (cmd + sha1)
266 def git_show_cdup():
267 '''Returns a relative path to the git project root.'''
268 return commands.getoutput ('git rev-parse --show-cdup')
270 def git_status():
271 '''RETURNS: A tuple of staged, unstaged and untracked files.
272 ( array(staged), array(unstaged), array(untracked) )'''
274 status_lines = commands.getoutput ('git status').split ('\n')
276 unstaged_header_seen = False
277 untracked_header_seen = False
279 modified_header = '# Changed but not updated:'
280 modified_regex = re.compile ('(#\tmodified:\W{3}'
281 + '|#\tnew file:\W{3}'
282 + '|#\tdeleted:\W{4})')
284 renamed_regex = re.compile ('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
286 untracked_header = '# Untracked files:'
287 untracked_regex = re.compile ('#\t(.+)')
289 staged = []
290 unstaged = []
291 untracked = []
293 # Untracked files
294 for status_line in status_lines:
295 if untracked_header in status_line:
296 untracked_header_seen = True
297 continue
298 if not untracked_header_seen:
299 continue
300 match = untracked_regex.match (status_line)
301 if match:
302 filename = match.group (1)
303 untracked.append (filename)
305 # Staged, unstaged, and renamed files
306 for status_line in status_lines:
307 if modified_header in status_line:
308 unstaged_header_seen = True
309 continue
310 match = modified_regex.match (status_line)
311 if match:
312 tag = match.group (0)
313 filename = status_line.replace (tag, '')
314 if unstaged_header_seen:
315 unstaged.append (filename)
316 else:
317 staged.append (filename)
318 continue
319 # Renamed files
320 match = renamed_regex.match (status_line)
321 if match:
322 oldname = match.group (2)
323 newname = match.group (3)
324 staged.append (oldname)
325 staged.append (newname)
327 return ( staged, unstaged, untracked )
329 def git_tag ():
330 return commands.getoutput ('git tag').split ('\n')