Refactored several methods out of the main controller.
[ugit.git] / ugitlibs / git.py
blob0b8d5712c49c55db7b7d34a37bdb3807ed6ea821
1 '''TODO: "import stgit"'''
2 import os
3 import re
4 import types
5 import utils
6 from cStringIO import StringIO
8 # A regex for matching the output of git(log|rev-list) --pretty=oneline
9 REV_LIST_REGEX = re.compile('([0-9a-f]+)\W(.*)')
11 def quote(argv):
12 return ' '.join([ utils.shell_quote(arg) for arg in argv ])
14 def git(*args,**kwargs):
15 return utils.run_cmd('git', *args, **kwargs)
17 def add(to_add):
18 '''Invokes 'git add' to index the filenames in to_add.'''
19 if not to_add: return 'No files to add.'
20 return git('add', *to_add)
22 def add_or_remove(to_process):
23 '''Invokes 'git add' to index the filenames in to_process that exist
24 and 'git rm' for those that do not exist.'''
26 if not to_process:
27 return 'No files to add or remove.'
29 to_add = []
30 to_remove = []
31 output = ''
33 for filename in to_process:
34 if os.path.exists(filename):
35 to_add.append(filename)
37 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
44 # Process files to remote
45 for filename in to_process:
46 if not os.path.exists(filename):
47 to_remove.append(filename)
48 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')
64 branches = git(*argv).splitlines()
65 return map(lambda(x): x.lstrip('* '), branches)
67 def cat_file(objtype, sha1):
68 return git('cat-file', objtype, sha1, raw=True)
70 def cherry_pick(revs, commit=False):
71 '''Cherry-picks each revision into the current branch.'''
72 if not revs:
73 return 'No revision selected.'
74 argv = [ 'cherry-pick' ]
75 if not commit: argv.append('-n')
77 cherries = []
78 for rev in revs:
79 new_argv = argv + [rev]
80 cherries.append(git(*new_argv))
82 return os.linesep.join(cherries)
84 def checkout(rev):
85 return git('checkout', rev)
87 def commit(msg, amend=False):
88 '''Creates a git commit.'''
90 if not msg.endswith(os.linesep):
91 msg += os.linesep
93 # Sure, this is a potential "security risk," but if someone
94 # is trying to intercept/re-write commit messages on your system,
95 # then you probably have bigger problems to worry about.
96 tmpfile = utils.get_tmp_filename()
97 argv = [ 'commit', '-F', tmpfile ]
98 if amend:
99 argv.append('--amend')
101 # Create the commit message file
102 file = open(tmpfile, 'w')
103 file.write(msg)
104 file.close()
106 # Run 'git commit'
107 output = git(*argv)
108 os.unlink(tmpfile)
110 return quote(argv) + os.linesep*2 + output
112 def create_branch(name, base, track=False):
113 '''Creates a branch starting from base. Pass track=True
114 to create a remote tracking branch.'''
115 if track:
116 return git('branch', '--track', name, base)
117 else:
118 return git('branch', name, base)
120 def current_branch():
121 '''Parses 'git branch' to find the current branch.'''
122 branches = git('branch').splitlines()
123 for branch in branches:
124 if branch.startswith('* '):
125 return branch.lstrip('* ')
126 return 'Detached HEAD'
128 def diff(commit=None,filename=None, color=False,
129 cached=True, with_diff_header=False,
130 reverse=False):
131 "Invokes git diff on a filepath."
133 argv = [ 'diff']
134 if reverse: argv.append('-R')
135 if color: argv.append('--color')
136 if cached: argv.append('--cached')
138 deleted = cached and not os.path.exists(filename)
140 if filename:
141 argv.append('--')
142 argv.append(filename)
144 if commit:
145 argv.append('%s^..%s' % (commit,commit))
147 diff = git(*argv)
148 diff_lines = diff.splitlines()
150 output = StringIO()
151 start = False
152 del_tag = 'deleted file mode '
154 headers = []
155 for line in diff_lines:
156 if not start and '@@ ' in line and ' @@' in line:
157 start = True
158 if start or(deleted and del_tag in line):
159 output.write(line + '\n')
160 else:
161 headers.append(line)
163 result = output.getvalue()
164 output.close()
166 if with_diff_header:
167 return(os.linesep.join(headers), result)
168 else:
169 return result
171 def diff_stat():
172 '''Returns the latest diffstat.'''
173 return git('diff','--stat','HEAD^')
175 def format_patch(revs, use_range):
176 '''Exports patches revs in the 'ugit-patches' subdirectory.
177 If use_range is True, a commit range is passed to git format-patch.'''
179 argv = ['format-patch','--thread','--patch-with-stat',
180 '-o','ugit-patches']
181 if len(revs) > 1:
182 argv.append('-n')
183 header = 'Generated Patches:'
184 if use_range:
185 new_argv = argv + ['%s^..%s' %( revs[-1], revs[0] )]
186 return git(*new_argv)
188 output = [ header ]
189 num_patches = 1
190 for idx, rev in enumerate(revs):
191 real_idx = str(idx + num_patches)
192 new_argv = argv + ['-1', '--start-number', real_idx, rev]
193 output.append(git(*new_argv))
194 num_patches += output[-1].count(os.linesep)
195 return os.linesep.join(output)
197 def config(key, value=None):
198 '''Gets or sets git config values. If value is not None, then
199 the config key will be set. Otherwise, the config value of the
200 config key is returned.'''
201 if value is not None:
202 return git('config', key, value)
203 else:
204 return git('config', '--get', key)
206 def log(oneline=True, all=False):
207 '''Returns a pair of parallel arrays listing the revision sha1's
208 and commit summaries.'''
209 argv = [ 'log' ]
210 if oneline:
211 argv.append('--pretty=oneline')
212 if all:
213 argv.append('--all')
214 revs = []
215 summaries = []
216 regex = REV_LIST_REGEX
217 output = git(*argv)
218 for line in output.splitlines():
219 match = regex.match(line)
220 if match:
221 revs.append(match.group(1))
222 summaries.append(match.group(2))
223 return( revs, summaries )
225 def ls_files():
226 return git('ls-files').splitlines()
228 def ls_tree(rev):
229 '''Returns a list of(mode, type, sha1, path) tuples.'''
231 lines = git('ls-tree', '-r', rev).splitlines()
232 output = []
233 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
234 for line in lines:
235 match = regex.match(line)
236 if match:
237 mode = match.group(1)
238 objtype = match.group(2)
239 sha1 = match.group(3)
240 filename = match.group(4)
241 output.append((mode, objtype, sha1, filename,) )
242 return output
244 def push(remote, local_branch, remote_branch, ffwd=True, tags=False):
245 argv = ['push']
246 if tags:
247 argv.append('--tags')
248 argv.append(remote)
250 if local_branch == remote_branch:
251 argv.append(local_branch)
252 else:
253 if not ffwd and local_branch:
254 argv.append('+%s:%s' % ( local_branch, remote_branch ))
255 else:
256 argv.append('%s:%s' % ( local_branch, remote_branch ))
258 return git(with_status=True, *argv)
260 def rebase(newbase):
261 if not newbase: return
262 return git('rebase', newbase)
264 def remote(*args):
265 argv = ['remote'] + list(args)
266 return git(*argv).splitlines()
268 def remote_url(name):
269 return config('remote.%s.url' % name)
271 def reset(to_unstage):
272 '''Use 'git reset' to unstage files from the index.'''
273 if not to_unstage:
274 return 'No files to reset.'
276 argv = [ 'reset', '--' ]
277 argv.extend(to_unstage)
279 return git(*argv)
281 def rev_list_range(start, end):
282 argv = [ 'rev-list', '--pretty=oneline', start, end ]
283 raw_revs = git(*argv).splitlines()
284 revs = []
285 for line in raw_revs:
286 match = REV_LIST_REGEX.match(line)
287 if match:
288 rev_id = match.group(1)
289 summary = match.group(2)
290 revs.append((rev_id, summary,) )
291 return revs
293 def show(sha1):
294 return git('show',sha1)
296 def show_cdup():
297 '''Returns a relative path to the git project root.'''
298 return git('rev-parse','--show-cdup')
300 def status():
301 '''RETURNS: A tuple of staged, unstaged and untracked files.
302 ( array(staged), array(unstaged), array(untracked) )'''
304 status_lines = git('status').splitlines()
306 unstaged_header_seen = False
307 untracked_header_seen = False
309 modified_header = '# Changed but not updated:'
310 modified_regex = re.compile('(#\tmodified:\W{3}'
311 + '|#\tnew file:\W{3}'
312 + '|#\tdeleted:\W{4})')
314 renamed_regex = re.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
316 untracked_header = '# Untracked files:'
317 untracked_regex = re.compile('#\t(.+)')
319 staged = []
320 unstaged = []
321 untracked = []
323 # Untracked files
324 for status_line in status_lines:
325 if untracked_header in status_line:
326 untracked_header_seen = True
327 continue
328 if not untracked_header_seen:
329 continue
330 match = untracked_regex.match(status_line)
331 if match:
332 filename = match.group(1)
333 untracked.append(filename)
335 # Staged, unstaged, and renamed files
336 for status_line in status_lines:
337 if modified_header in status_line:
338 unstaged_header_seen = True
339 continue
340 match = modified_regex.match(status_line)
341 if match:
342 tag = match.group(0)
343 filename = status_line.replace(tag, '')
344 if unstaged_header_seen:
345 unstaged.append(filename)
346 else:
347 staged.append(filename)
348 continue
349 # Renamed files
350 match = renamed_regex.match(status_line)
351 if match:
352 oldname = match.group(2)
353 newname = match.group(3)
354 staged.append(oldname)
355 staged.append(newname)
357 return( staged, unstaged, untracked )
359 def tag():
360 return git('tag').splitlines()