clean up the comments
[ugit.git] / ugit / git.py
blob92c7db2fa3fef327768846316bd626b49e616514
1 import os
2 import re
3 import sys
4 import types
5 import utils
6 import defaults
7 from cStringIO import StringIO
9 def git(*args,**kwargs):
10 """This is a convenience wrapper around run_cmd that
11 sets things up so that commands are run in the canonical
12 'git command [options] [args]' form."""
13 cmd = 'git %s' % args[0]
14 return utils.run_cmd(cmd, *args[1:], **kwargs)
16 class GitCommand(object):
17 """This class wraps this module so that arbitrary git commands
18 can be dynamically called at runtime."""
19 def __init__(self, module):
20 self.module = module
21 def __getattr__(self, name):
22 def git_cmd(*args, **kwargs):
23 return git(name.replace('_','-'), *args, **kwargs)
24 try:
25 return getattr(self.module, name)
26 except AttributeError:
27 return git_cmd
29 # At import we replace this module with a GitCommand singleton.
30 gitcmd = GitCommand(sys.modules[__name__])
31 sys.modules[__name__] = gitcmd
34 #+-------------------------------------------------------------------------
35 #+ A regex for matching the output of git(log|rev-list) --pretty=oneline
36 REV_LIST_REGEX = re.compile('([0-9a-f]+)\W(.*)')
38 def quote(argv):
39 return ' '.join([ utils.shell_quote(arg) for arg in argv ])
41 def add(to_add, verbose=True):
42 """Invokes 'git add' to index the filenames in to_add."""
43 if not to_add:
44 return 'No files to add.'
45 return git('add', verbose=verbose, *to_add)
47 def add_or_remove(to_process):
48 """Invokes 'git add' to index the filenames in to_process that exist
49 and 'git rm' for those that do not exist."""
51 if not to_process:
52 return 'No files to add or remove.'
54 to_add = []
55 to_remove = []
57 for filename in to_process:
58 if os.path.exists(filename):
59 to_add.append(filename)
61 output = add(to_add)
63 if len(to_add) == len(to_process):
64 # to_process only contained unremoved files --
65 # short-circuit the removal checks
66 return output
68 # Process files to remote
69 for filename in to_process:
70 if not os.path.exists(filename):
71 to_remove.append(filename)
72 output + '\n\n' + git('rm',*to_remove)
74 def apply(filename, indexonly=True, reverse=False):
75 kwargs = {}
76 if reverse:
77 kwargs['reverse'] = True
78 if indexonly:
79 kwargs['index'] = True
80 kwargs['cached'] = True
81 argv = ['apply', filename]
82 return git(*argv, **kwargs)
84 def branch(name=None, remote=False, delete=False):
85 if delete and name:
86 return git('branch', name, D=True)
87 else:
88 branches = map(lambda x: x.lstrip('* '),
89 git('branch', r=remote).splitlines())
90 if remote:
91 remotes = []
92 for branch in branches:
93 if branch.endswith('/HEAD'):
94 continue
95 remotes.append(branch)
96 return remotes
97 return branches
99 def cat_file(objtype, sha1):
100 return git('cat-file', objtype, sha1, raw=True)
102 def cherry_pick(revs, commit=False):
103 """Cherry-picks each revision into the current branch.
104 Returns a list of command output strings (1 per cherry pick)"""
106 if not revs: return []
108 argv = [ 'cherry-pick' ]
109 kwargs = {}
110 if not commit:
111 kwargs['n'] = True
113 cherries = []
114 for rev in revs:
115 new_argv = argv + [rev]
116 cherries.append(git(*new_argv, **kwargs))
118 return '\n'.join(cherries)
120 def checkout(*args):
121 return git('checkout', *args)
123 def commit(msg, amend=False):
124 """Creates a git commit."""
126 if not msg.endswith('\n'):
127 msg += '\n'
129 # Sure, this is a potential "security risk," but if someone
130 # is trying to intercept/re-write commit messages on your system,
131 # then you probably have bigger problems to worry about.
132 tmpfile = utils.get_tmp_filename()
133 kwargs = {
134 'F': tmpfile,
135 'amend': amend,
138 # Create the commit message file
139 file = open(tmpfile, 'w')
140 file.write(msg)
141 file.close()
143 # Run 'git commit'
144 output = git('commit', F=tmpfile, amend=amend)
145 os.unlink(tmpfile)
147 return ('git commit -F %s --amend %s\n\n%s'
148 % ( tmpfile, amend, output ))
150 def create_branch(name, base, track=False):
151 """Creates a branch starting from base. Pass track=True
152 to create a remote tracking branch."""
153 return git('branch', name, base, track=track)
155 def current_branch():
156 """Parses 'git branch' to find the current branch."""
157 branches = git('branch').splitlines()
158 for branch in branches:
159 if branch.startswith('* '):
160 return branch.lstrip('* ')
161 return 'Detached HEAD'
163 def diff(commit=None,filename=None, color=False,
164 cached=True, with_diff_header=False,
165 suppress_header=True, reverse=False):
166 "Invokes git diff on a filepath."
168 argv = []
169 if commit:
170 argv.append('%s^..%s' % (commit, commit))
172 if filename:
173 argv.append('--')
174 if type(filename) is list:
175 argv.extend(filename)
176 else:
177 argv.append(filename)
179 kwargs = {
180 'patch-with-raw': True,
181 'unified': defaults.DIFF_CONTEXT,
184 diff = git('diff',
185 R=reverse,
186 color=color,
187 cached=cached,
188 *argv,
189 **kwargs)
191 diff_lines = diff.splitlines()
193 output = StringIO()
194 start = False
195 del_tag = 'deleted file mode '
197 headers = []
198 deleted = cached and not os.path.exists(filename)
199 for line in diff_lines:
200 if not start and '@@ ' in line and ' @@' in line:
201 start = True
202 if start or(deleted and del_tag in line):
203 output.write(line + '\n')
204 else:
205 if with_diff_header:
206 headers.append(line)
207 elif not suppress_header:
208 output.write(line + '\n')
209 result = output.getvalue()
210 output.close()
211 if with_diff_header:
212 return('\n'.join(headers), result)
213 else:
214 return result
216 def diffstat():
217 return git('diff', 'HEAD^',
218 unified=defaults.DIFF_CONTEXT,
219 stat=True)
221 def diffindex():
222 return git('diff',
223 unified=defaults.DIFF_CONTEXT,
224 stat=True,
225 cached=True)
227 def format_patch(revs):
228 """writes patches named by revs to the "patches" directory."""
229 num_patches = 1
230 output = []
231 kwargs = {
232 'o': 'patches',
233 'n': len(revs) > 1,
234 'thread': True,
235 'patch-with-stat': True,
237 for idx, rev in enumerate(revs):
238 real_idx = idx + num_patches
239 kwargs['start-number'] = real_idx
240 revarg = '%s^..%s'%(rev,rev)
241 output.append(git('format-patch', revarg, **kwargs))
242 num_patches += output[-1].count('\n')
243 return '\n'.join(output)
245 def config(key=None, value=None, local=False, asdict=False):
246 if key:
247 argv = ['config', key]
248 else:
249 argv = ['config']
251 kwargs = {
252 'global': local is False,
253 'get': key and value is None,
254 'list': asdict,
257 if asdict:
258 return config_to_dict(git('config', **kwargs).splitlines())
260 elif kwargs['get']:
261 return git('config', key, **kwargs)
263 elif key and value is not None:
264 # git config category.key value
265 strval = str(value)
266 if type(value) is bool:
267 # git uses "true" and "false"
268 strval = strval.lower()
269 return git('config', key, strval, **kwargs)
270 else:
271 msg = "oops in git.config(key=%s,value=%s,local=%s,asdict=%s"
272 raise Exception(msg % (key, value, local, asdict))
275 def config_to_dict(config_lines):
276 """parses the lines from git config --list into a dictionary"""
278 newdict = {}
279 for line in config_lines:
280 k, v = line.split('=')
281 k = k.replace('.','_') # git -> model
282 if v == 'true' or v == 'false':
283 v = bool(eval(v.title()))
284 try:
285 v = int(eval(v))
286 except:
287 pass
288 newdict[k]=v
289 return newdict
291 def log(oneline=True, all=False):
292 """Returns a pair of parallel arrays listing the revision sha1's
293 and commit summaries."""
294 kwargs = {}
295 if oneline:
296 kwargs['pretty'] = 'oneline'
297 revs = []
298 summaries = []
299 regex = REV_LIST_REGEX
300 output = git('log', all=all, **kwargs)
301 for line in output.splitlines():
302 match = regex.match(line)
303 if match:
304 revs.append(match.group(1))
305 summaries.append(match.group(2))
306 return( revs, summaries )
308 def ls_files():
309 """git ls-files as a list"""
310 return git('ls-files').splitlines()
312 def ls_tree(rev):
313 """Returns a list of(mode, type, sha1, path) tuples."""
314 lines = git('ls-tree', rev, r=True).splitlines()
315 output = []
316 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
317 for line in lines:
318 match = regex.match(line)
319 if match:
320 mode = match.group(1)
321 objtype = match.group(2)
322 sha1 = match.group(3)
323 filename = match.group(4)
324 output.append((mode, objtype, sha1, filename,) )
325 return output
327 def push(remote, local_branch, remote_branch, ffwd=True, tags=False):
328 if ffwd:
329 branch_arg = '%s:%s' % ( local_branch, remote_branch )
330 else:
331 branch_arg = '+%s:%s' % ( local_branch, remote_branch )
332 return git('push', remote, branch_arg, with_status=True, tags=tags)
334 def rebase(newbase):
335 if not newbase:
336 return 'No base branch specified to rebase.'
337 return git('rebase', newbase)
339 def remote(*args):
340 return git('remote', without_stderr=True, *args).splitlines()
342 def remote_url(name):
343 return config('remote.%s.url' % name, local=True)
345 def reset(to_unstage):
346 """Use 'git reset' to unstage files from the index."""
347 if not to_unstage:
348 return 'No files to reset.'
350 argv = [ 'reset', '--' ]
351 argv.extend(to_unstage)
352 return git(*argv)
354 def rev_list_range(start, end):
355 argv = [ 'rev-list', '--pretty=oneline', start, end ]
356 raw_revs = git(*argv).splitlines()
357 revs = []
358 for line in raw_revs:
359 match = REV_LIST_REGEX.match(line)
360 if match:
361 rev_id = match.group(1)
362 summary = match.group(2)
363 revs.append((rev_id, summary,) )
364 return revs
366 def parsed_status():
367 """RETURNS: A tuple of staged, unstaged and untracked file lists."""
369 MODIFIED_TAG = '# Changed but not updated:'
370 UNTRACKED_TAG = '# Untracked files:'
372 RGX_RENAMED = re.compile(
373 '(#\trenamed:\s+)'
374 '(.*?)\s->\s(.*)'
377 RGX_MODIFIED = re.compile(
378 '(#\tmodified:\s+'
379 '|#\tnew file:\s+'
380 '|#\tdeleted:\s+)'
382 staged = []
383 unstaged = []
384 untracked = []
386 STAGED_MODE = 0
387 UNSTAGED_MODE = 1
388 UNTRACKED_MODE = 2
390 mode = STAGED_MODE
391 current_dest = staged
393 for status_line in gitcmd.status().splitlines():
394 if status_line == MODIFIED_TAG:
395 mode = UNSTAGED_MODE
396 current_dest = unstaged
397 continue
399 elif status_line == UNTRACKED_TAG:
400 mode = UNTRACKED_MODE
401 current_dest = untracked
402 continue
404 # Staged/unstaged modified/renamed/deleted files
405 if mode == STAGED_MODE or mode == UNSTAGED_MODE:
406 match = RGX_MODIFIED.match(status_line)
407 if match:
408 tag = match.group(0)
409 filename = status_line.replace(tag, '')
410 current_dest.append(filename)
411 continue
412 match = RGX_RENAMED.match(status_line)
413 if match:
414 oldname = match.group(2)
415 newname = match.group(3)
416 current_dest.append(oldname)
417 current_dest.append(newname)
418 continue
419 # Untracked files
420 elif mode is UNTRACKED_MODE:
421 if status_line.startswith('#\t'):
422 current_dest.append(status_line[2:])
424 return( staged, unstaged, untracked )