Merged cherrypick into master and added copy/paste handling to GitCommitBrowser
[ugit.git] / py / ugitcmds.py
blob712ee362e83a2197e3ab7f5a90a20ec24b16fe9f
1 import os
2 import re
3 import time
4 import commands
5 import ugitutils
7 def git_add (to_add):
8 '''Invokes 'git add' to index the filenames in to_add.'''
10 if not to_add: return 'ERROR: No files to add.'
12 argv = [ 'git', 'add' ]
13 for filename in to_add:
14 argv.append (ugitutils.shell_quote (filename))
16 cmd = ' '.join (argv)
17 return 'Running:\t%s\n%s\n%s added successfully' % (
18 cmd, commands.getoutput (cmd), ', '.join (to_add) )
20 def git_add_or_remove (to_process):
21 '''Invokes 'git add' to index the filenames in to_process that exist
22 and 'git rm' for those that do not exist.'''
24 if not to_process: return 'ERROR: No files to add or remove.'
26 to_add = []
27 output = ''
29 for filename in to_process:
30 if os.path.exists (filename):
31 to_add.append (filename)
33 if to_add:
34 output += git_add (to_add) + '\n\n'
36 if len(to_add) == len(to_process):
37 # to_process only contained unremoved files --
38 # short-circuit the removal checks
39 return output
41 # Process files to add
42 argv = [ 'git', 'rm' ]
43 for filename in to_process:
44 if not os.path.exists (filename):
45 argv.append (ugitutils.shell_quote (filename))
47 cmd = ' '.join (argv)
48 return output + 'Running: %s\n%s' % ( cmd, commands.getoutput (cmd) )
50 def git_branch():
51 '''Returns 'git branch''s output in a list.'''
52 return commands.getoutput ('git branch').split ('\n')
54 def git_cherry_pick (revs, commit=False):
55 '''Cherry-picks each revision into the current branch.'''
56 if not revs:
57 return 'ERROR: No revisions selected for cherry-picking.'''
59 cmd = 'git cherry-pick '
60 if not commit: cmd += '-n '
61 output = []
62 for rev in revs:
63 output.append ('Cherry-picking: ' + rev)
64 output.append (commands.getoutput (cmd + rev))
65 output.append ('')
66 return '\n'.join (output)
68 def git_commit (msg, amend, commit_all, files):
69 '''Creates a git commit. 'commit_all' triggers the -a
70 flag to 'git commit.' 'amend' triggers --amend.
71 'files' is a list of files to use for commits without -a.'''
73 if not msg:
74 return 'ERROR: No commit message was provided.'
76 # Allow TMPDIR/TMP with a fallback to /tmp
77 tmpdir = os.getenv ('TMPDIR', os.getenv ('TMP', '/tmp'))
79 # Sure, this is a potential "security risk," but if someone
80 # is trying to intercept/re-write commit messages on your system,
81 # then you probably have bigger problems to worry about.
82 tmpfile = os.path.join (tmpdir,
83 'ugit.%s.%s' % ( os.getuid(), time.time() ))
85 argv = [ 'git', 'commit', '-F', tmpfile ]
87 if amend: argv.append ('--amend')
89 if commit_all:
90 argv.append ('-a')
91 else:
92 if not files:
93 return 'ERROR: No files selected for commit.'
95 argv.append ('--')
96 for file in files:
97 argv.append (ugitutils.shell_quote (file))
99 # Create the commit message file
100 file = open (tmpfile, 'w')
101 file.write (msg)
102 file.close()
104 # Run 'git commit'
105 cmd = ' '.join (argv)
106 output = commands.getoutput (cmd)
107 os.unlink (tmpfile)
109 return 'Running:\t%s\n%s' % ( cmd, output )
111 def git_current_branch():
112 '''Parses 'git branch' to find the current branch.'''
113 for branch in git_branch():
114 if branch.startswith ('* '):
115 return branch.lstrip ('* ')
116 raise Exception, 'No current branch. Detached HEAD?'
118 def git_diff (filename, staged=True):
119 '''Invokes git_diff on filename. Passing staged=True adds
120 diffs the index against HEAD (i.e. --cached).'''
122 deleted = False
123 argv = [ 'git', 'diff', '--color']
124 if staged:
125 deleted = not os.path.exists (filename)
126 argv.append ('--cached')
128 argv.append ('--')
129 argv.append (ugitutils.shell_quote (filename))
131 diff = commands.getoutput (' '.join (argv))
132 diff_lines = diff.split ('\n')
134 output = []
135 start = False
136 del_tag = 'deleted file mode '
138 for line in diff_lines:
139 if not start and '@@ ' in line and ' @@' in line:
140 start = True
141 if start or (deleted and del_tag in line):
142 output.append (line)
143 return '\n'.join (output)
145 def git_diff_stat ():
146 '''Returns the latest diffstat.'''
147 return commands.getoutput ('git diff --color --stat HEAD^')
149 def git_format_patch (revs, use_range):
150 '''Exports patches revs in the 'ugit-patches' subdirectory.
151 If use_range is True, a commit range is passed to git format-patch.'''
153 cmd = 'git format-patch --thread --patch-with-stat -o ugit-patches '
154 header = 'Generated Patches:'
155 if len (revs) > 1:
156 cmd += '-n '
158 if use_range:
159 rev_range = '%s^..%s' % ( revs[-1], revs[0] )
160 return header + '\n' + commands.getoutput (cmd + rev_range)
162 output = [ header ]
163 num_patches = 1
164 for idx, rev in enumerate (revs):
165 real_idx = idx + num_patches
166 revcmd = cmd + '-1 --start-number %d %s' % (real_idx, rev)
167 output.append (commands.getoutput (revcmd))
168 num_patches += output[-1].count ('\n')
169 return '\n'.join (output)
171 def git_log (oneline=True, all=False):
172 '''Returns a pair of parallel arrays listing the revision sha1's
173 and commit summaries.'''
174 argv = [ 'git', 'log' ]
175 if oneline: argv.append ('--pretty=oneline')
176 if all: argv.append ('--all')
177 revs = []
178 summaries = []
179 regex = re.compile ('(\w+)\W(.*)')
180 output = commands.getoutput (' '.join (argv))
181 for line in output.split ('\n'):
182 match = regex.match (line)
183 if match:
184 revs.append (match.group (1))
185 summaries.append (match.group (2))
186 return ( revs, summaries )
188 def git_reset (to_unstage):
189 '''Use 'git reset' to unstage files from the index.'''
191 if not to_unstage: return 'ERROR: No files to reset.'
193 argv = [ 'git', 'reset', '--' ]
194 for filename in to_unstage:
195 argv.append (ugitutils.shell_quote (filename))
197 cmd = ' '.join (argv)
198 return 'Running:\t%s\n%s' % ( cmd, commands.getoutput (cmd) )
200 def git_show (sha1, color=False):
201 cmd = 'git show '
202 if color: cmd += '--color '
203 return commands.getoutput (cmd + sha1)
205 def git_show_cdup():
206 '''Returns a relative path to the git project root.'''
207 return commands.getoutput ('git rev-parse --show-cdup')
209 def git_status():
210 '''RETURNS: A tuple of staged, unstaged and untracked files.
211 ( array(staged), array(unstaged), array(untracked) )'''
213 status_lines = commands.getoutput ('git status').split ('\n')
215 unstaged_header_seen = False
216 untracked_header_seen = False
218 modified_header = '# Changed but not updated:'
219 modified_regex = re.compile('(#\tmodified:|#\tnew file:|#\tdeleted:)')
221 untracked_header = '# Untracked files:'
222 untracked_regex = re.compile ('#\t(.+)')
224 staged = []
225 unstaged = []
226 untracked = []
228 for status_line in status_lines:
229 if untracked_header in status_line:
230 untracked_header_seen = True
231 continue
232 if not untracked_header_seen:
233 continue
234 match = untracked_regex.match (status_line)
235 if match:
236 filename = match.group (1)
237 untracked.append (filename)
239 for status_line in status_lines:
240 if modified_header in status_line:
241 unstaged_header_seen = True
242 continue
243 match = modified_regex.match (status_line)
244 if match:
245 tag = match.group (0)
246 filename = status_line.replace (tag, '')
247 if unstaged_header_seen:
248 unstaged.append (filename.lstrip())
249 else:
250 staged.append (filename.lstrip())
252 return ( staged, unstaged, untracked )