Fix the non-ffwd push operation
[ugit.git] / ugitlibs / git.py
blob9a6075ba373157bc10e3685d9976d31e1d3e7152
1 '''TODO: "import stgit"'''
2 import os
3 import re
4 import types
5 import utils
6 import defaults
7 from cStringIO import StringIO
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 git(*args,**kwargs):
16 return utils.run_cmd('git', *args, **kwargs)
18 def add(to_add):
19 '''Invokes 'git add' to index the filenames in to_add.'''
20 if not to_add: return 'No files to add.'
21 return git('add', '-v', *to_add)
23 def 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:
28 return 'No files to add or remove.'
30 to_add = []
31 to_remove = []
33 for filename in to_process:
34 if os.path.exists(filename):
35 to_add.append(filename)
37 output = add(to_add)
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 remote
45 for filename in to_process:
46 if not os.path.exists(filename):
47 to_remove.append(filename)
48 output + '\n\n' + git('rm',*to_remove)
50 def apply(filename, indexonly=True, reverse=False):
51 argv = ['apply']
52 if reverse: argv.append('--reverse')
53 if indexonly: argv.extend(['--index', '--cached'])
54 argv.append(filename)
55 return git(*argv)
57 def branch(name=None, remote=False, delete=False):
58 if delete and name:
59 return git('branch', '-D', name)
60 else:
61 argv = ['branch']
62 if remote: argv.append('-r')
63 branches = map(lambda x: x.lstrip('* '),
64 git(*argv).splitlines())
65 if remote:
66 remotes = []
67 for branch in branches:
68 if branch.endswith('/HEAD'): continue
69 remotes.append(branch)
70 return remotes
71 return branches
73 def cat_file(objtype, sha1):
74 return git('cat-file', objtype, sha1, raw=True)
76 def cherry_pick(revs, commit=False):
77 '''Cherry-picks each revision into the current branch.'''
78 if not revs:
79 return 'No revision selected.'
80 argv = [ 'cherry-pick' ]
81 if not commit: argv.append('-n')
83 cherries = []
84 for rev in revs:
85 new_argv = argv + [rev]
86 cherries.append(git(*new_argv))
88 return '\n'.join(cherries)
90 def checkout(rev):
91 return git('checkout', rev)
93 def commit(msg, amend=False):
94 '''Creates a git commit.'''
96 if not msg.endswith('\n'):
97 msg += '\n'
99 # Sure, this is a potential "security risk," but if someone
100 # is trying to intercept/re-write commit messages on your system,
101 # then you probably have bigger problems to worry about.
102 tmpfile = utils.get_tmp_filename()
103 argv = [ 'commit', '-F', tmpfile ]
104 if amend:
105 argv.append('--amend')
107 # Create the commit message file
108 file = open(tmpfile, 'w')
109 file.write(msg)
110 file.close()
112 # Run 'git commit'
113 output = git(*argv)
114 os.unlink(tmpfile)
116 return 'git ' + quote(argv) + '\n\n' + output
118 def create_branch(name, base, track=False):
119 '''Creates a branch starting from base. Pass track=True
120 to create a remote tracking branch.'''
121 if track: return git('branch', '--track', name, base)
122 else: return git('branch', name, base)
124 def current_branch():
125 '''Parses 'git branch' to find the current branch.'''
126 branches = git('branch').splitlines()
127 for branch in branches:
128 if branch.startswith('* '):
129 return branch.lstrip('* ')
130 return 'Detached HEAD'
132 def diff(commit=None,filename=None, color=False,
133 cached=True, with_diff_header=False,
134 suppress_header=True, reverse=False):
135 "Invokes git diff on a filepath."
137 argv = [ 'diff', '--unified='+str(defaults.DIFF_CONTEXT), '--patch-with-raw']
138 if reverse: argv.append('-R')
139 if color: argv.append('--color')
140 if cached: argv.append('--cached')
142 deleted = cached and not os.path.exists(filename)
144 if filename:
145 argv.append('--')
146 argv.append(filename)
148 if commit:
149 argv.append('%s^..%s' % (commit,commit))
151 diff = git(*argv)
152 diff_lines = diff.splitlines()
154 output = StringIO()
155 start = False
156 del_tag = 'deleted file mode '
158 headers = []
159 for line in diff_lines:
160 if not start and '@@ ' in line and ' @@' in line:
161 start = True
162 if start or(deleted and del_tag in line):
163 output.write(line + '\n')
164 else:
165 if with_diff_header:
166 headers.append(line)
167 elif not suppress_header:
168 output.write(line + '\n')
170 result = output.getvalue()
171 output.close()
173 if with_diff_header:
174 return('\n'.join(headers), result)
175 else:
176 return result
178 def diffstat():
179 return git('diff','--unified='+str(defaults.DIFF_CONTEXT),
180 '--stat', 'HEAD^')
182 def diffindex():
183 return git('diff', '--unified='+str(defaults.DIFF_CONTEXT),
184 '--stat', '--cached')
186 def format_patch(revs):
187 '''writes patches named by revs to the "patches" directory.'''
188 num_patches = 1
189 output = []
190 argv = ['format-patch','--thread','--patch-with-stat', '-o','patches']
191 if len(revs) > 1:
192 argv.append('-n')
193 for idx, rev in enumerate(revs):
194 real_idx = str(idx + num_patches)
195 new_argv = argv + ['--start-number', real_idx,
196 '%s^..%s'%(rev,rev)]
197 output.append(git(*new_argv))
198 num_patches += output[-1].count('\n')
199 return '\n'.join(output)
201 def config(key, value=None, local=True):
202 argv = ['config']
203 if not local:
204 argv.append('--global')
205 if value is None:
206 argv.append('--get')
207 argv.append(key)
208 else:
209 argv.append(key)
210 if type(value) is bool:
211 value = str(value).lower()
212 argv.append(str(value))
213 return git(*argv)
215 def log(oneline=True, all=False):
216 '''Returns a pair of parallel arrays listing the revision sha1's
217 and commit summaries.'''
218 argv = [ 'log' ]
219 if oneline:
220 argv.append('--pretty=oneline')
221 if all:
222 argv.append('--all')
223 revs = []
224 summaries = []
225 regex = REV_LIST_REGEX
226 output = git(*argv)
227 for line in output.splitlines():
228 match = regex.match(line)
229 if match:
230 revs.append(match.group(1))
231 summaries.append(match.group(2))
232 return( revs, summaries )
234 def ls_files():
235 return git('ls-files').splitlines()
237 def ls_tree(rev):
238 '''Returns a list of(mode, type, sha1, path) tuples.'''
239 lines = git('ls-tree', '-r', rev).splitlines()
240 output = []
241 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
242 for line in lines:
243 match = regex.match(line)
244 if match:
245 mode = match.group(1)
246 objtype = match.group(2)
247 sha1 = match.group(3)
248 filename = match.group(4)
249 output.append((mode, objtype, sha1, filename,) )
250 return output
252 def push(remote, local_branch, remote_branch, ffwd=True, tags=False):
253 if ffwd:
254 branch_arg = '%s:%s' % ( local_branch, remote_branch )
255 else:
256 branch_arg = '+%s:%s' % ( local_branch, remote_branch )
257 argv = ['push']
258 if tags:
259 argv.append('--tags')
260 argv.append(remote)
261 argv.append(branch_arg)
263 return git(with_status=True, *argv)
265 def rebase(newbase):
266 if not newbase: return 'No base branch specified to rebase.'
267 return git('rebase', newbase)
269 def remote(*args):
270 return git('remote', stderr=False, *args).splitlines()
272 def remote_url(name):
273 return config('remote.%s.url' % name)
275 def reset(to_unstage):
276 '''Use 'git reset' to unstage files from the index.'''
277 if not to_unstage:
278 return 'No files to reset.'
280 argv = [ 'reset', '--' ]
281 argv.extend(to_unstage)
282 return git(*argv)
284 def rev_list_range(start, end):
285 argv = [ 'rev-list', '--pretty=oneline', start, end ]
286 raw_revs = git(*argv).splitlines()
287 revs = []
288 for line in raw_revs:
289 match = REV_LIST_REGEX.match(line)
290 if match:
291 rev_id = match.group(1)
292 summary = match.group(2)
293 revs.append((rev_id, summary,) )
294 return revs
296 def show(sha1):
297 return git('show',sha1)
299 def show_cdup():
300 '''Returns a relative path to the git project root.'''
301 return git('rev-parse','--show-cdup')
303 def status():
304 '''RETURNS: A tuple of staged, unstaged and untracked files.
305 ( array(staged), array(unstaged), array(untracked) )'''
307 status_lines = git('status').splitlines()
309 unstaged_header_seen = False
310 untracked_header_seen = False
312 modified_header = '# Changed but not updated:'
313 modified_regex = re.compile('(#\tmodified:\s+'
314 '|#\tnew file:\s+'
315 '|#\tdeleted:\s+)')
317 renamed_regex = re.compile('(#\trenamed:\s+)(.*?)\s->\s(.*)')
319 untracked_header = '# Untracked files:'
320 untracked_regex = re.compile('#\t(.+)')
322 staged = []
323 unstaged = []
324 untracked = []
326 # Untracked files
327 for status_line in status_lines:
328 if untracked_header in status_line:
329 untracked_header_seen = True
330 continue
331 if not untracked_header_seen:
332 continue
333 match = untracked_regex.match(status_line)
334 if match:
335 filename = match.group(1)
336 untracked.append(filename)
338 # Staged, unstaged, and renamed files
339 for status_line in status_lines:
340 if modified_header in status_line:
341 unstaged_header_seen = True
342 continue
343 match = modified_regex.match(status_line)
344 if match:
345 tag = match.group(0)
346 filename = status_line.replace(tag, '')
347 if unstaged_header_seen:
348 unstaged.append(filename)
349 else:
350 staged.append(filename)
351 continue
352 # Renamed files
353 match = renamed_regex.match(status_line)
354 if match:
355 oldname = match.group(2)
356 newname = match.group(3)
357 staged.append(oldname)
358 staged.append(newname)
360 return( staged, unstaged, untracked )
362 def tag():
363 return git('tag').splitlines()