Added option edit/save support via git config.
[ugit.git] / ugitlibs / git.py
blobc70eee16b310865f10dc264355d69f97adbb7111
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(.*)')
10 DIFF_CONTEXT = 5
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', *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 = []
32 output = ''
34 for filename in to_process:
35 if os.path.exists(filename):
36 to_add.append(filename)
38 add(to_add)
40 if len(to_add) == len(to_process):
41 # to_process only contained unremoved files --
42 # short-circuit the removal checks
43 return
45 # Process files to remote
46 for filename in to_process:
47 if not os.path.exists(filename):
48 to_remove.append(filename)
49 git('rm',*to_remove)
51 def apply(filename, indexonly=True, reverse=False):
52 argv = ['apply']
53 if reverse: argv.append('--reverse')
54 if indexonly: argv.extend(['--index', '--cached'])
55 argv.append(filename)
56 return git(*argv)
58 def branch(name=None, remote=False, delete=False):
59 if delete and name:
60 return git('branch', '-D', name)
61 else:
62 argv = ['branch']
63 if remote: argv.append('-r')
65 branches = git(*argv).splitlines()
66 return map(lambda(x): x.lstrip('* '), branches)
68 def cat_file(objtype, sha1):
69 return git('cat-file', objtype, sha1, raw=True)
71 def cherry_pick(revs, commit=False):
72 '''Cherry-picks each revision into the current branch.'''
73 if not revs:
74 return 'No revision selected.'
75 argv = [ 'cherry-pick' ]
76 if not commit: argv.append('-n')
78 cherries = []
79 for rev in revs:
80 new_argv = argv + [rev]
81 cherries.append(git(*new_argv))
83 return os.linesep.join(cherries)
85 def checkout(rev):
86 return git('checkout', rev)
88 def commit(msg, amend=False):
89 '''Creates a git commit.'''
91 if not msg.endswith(os.linesep):
92 msg += os.linesep
94 # Sure, this is a potential "security risk," but if someone
95 # is trying to intercept/re-write commit messages on your system,
96 # then you probably have bigger problems to worry about.
97 tmpfile = utils.get_tmp_filename()
98 argv = [ 'commit', '-F', tmpfile ]
99 if amend:
100 argv.append('--amend')
102 # Create the commit message file
103 file = open(tmpfile, 'w')
104 file.write(msg)
105 file.close()
107 # Run 'git commit'
108 output = git(*argv)
109 os.unlink(tmpfile)
111 return quote(argv) + os.linesep*2 + output
113 def create_branch(name, base, track=False):
114 '''Creates a branch starting from base. Pass track=True
115 to create a remote tracking branch.'''
116 if track:
117 return git('branch', '--track', name, base)
118 else:
119 return git('branch', name, base)
121 def current_branch():
122 '''Parses 'git branch' to find the current branch.'''
123 branches = git('branch').splitlines()
124 for branch in branches:
125 if branch.startswith('* '):
126 return branch.lstrip('* ')
127 return 'Detached HEAD'
129 def diff(commit=None,filename=None, color=False,
130 cached=True, with_diff_header=False,
131 suppress_header=True, reverse=False):
132 "Invokes git diff on a filepath."
134 argv = [ 'diff', '--unified='+str(DIFF_CONTEXT), '--patch-with-raw']
135 if reverse: argv.append('-R')
136 if color: argv.append('--color')
137 if cached: argv.append('--cached')
139 deleted = cached and not os.path.exists(filename)
141 if filename:
142 argv.append('--')
143 argv.append(filename)
145 if commit:
146 argv.append('%s^..%s' % (commit,commit))
148 diff = git(*argv)
149 diff_lines = diff.splitlines()
151 output = StringIO()
152 start = False
153 del_tag = 'deleted file mode '
155 headers = []
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.write(line)
161 output.write(os.linesep)
162 else:
163 if with_diff_header:
164 headers.append(line)
165 elif not suppress_header:
166 output.write(line)
167 output.write(os.linesep)
169 result = output.getvalue()
170 output.close()
172 if with_diff_header:
173 return(os.linesep.join(headers), result)
174 else:
175 return result
177 def diff_stat():
178 return git('diff','--stat','HEAD^')
180 def format_patch(revs):
181 '''writes patches named by revs to the "patches" directory.'''
182 num_patches = 1
183 output = []
184 argv = ['format-patch','--thread','--patch-with-stat', '-o','patches']
185 if len(revs) > 1:
186 argv.append('-n')
187 for idx, rev in enumerate(revs):
188 real_idx = str(idx + num_patches)
189 new_argv = argv + ['--start-number', real_idx,
190 '%s^..%s'%(rev,rev)]
191 output.append(git(*new_argv))
192 num_patches += output[-1].count(os.linesep)
193 return os.linesep.join(output)
195 def config(key, value=None, local=True):
196 argv = ['config']
197 if not local:
198 argv.append('--global')
199 if value is None:
200 argv.append('--get')
201 argv.append(key)
202 else:
203 argv.append(key)
204 if type(value) is bool:
205 value = str(value).lower()
206 argv.append(str(value))
207 return git(*argv)
209 def log(oneline=True, all=False):
210 '''Returns a pair of parallel arrays listing the revision sha1's
211 and commit summaries.'''
212 argv = [ 'log' ]
213 if oneline:
214 argv.append('--pretty=oneline')
215 if all:
216 argv.append('--all')
217 revs = []
218 summaries = []
219 regex = REV_LIST_REGEX
220 output = git(*argv)
221 for line in output.splitlines():
222 match = regex.match(line)
223 if match:
224 revs.append(match.group(1))
225 summaries.append(match.group(2))
226 return( revs, summaries )
228 def ls_files():
229 return git('ls-files').splitlines()
231 def ls_tree(rev):
232 '''Returns a list of(mode, type, sha1, path) tuples.'''
233 lines = git('ls-tree', '-r', rev).splitlines()
234 output = []
235 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
236 for line in lines:
237 match = regex.match(line)
238 if match:
239 mode = match.group(1)
240 objtype = match.group(2)
241 sha1 = match.group(3)
242 filename = match.group(4)
243 output.append((mode, objtype, sha1, filename,) )
244 return output
246 def push(remote, local_branch, remote_branch, ffwd=True, tags=False):
247 argv = ['push']
248 if tags:
249 argv.append('--tags')
250 argv.append(remote)
251 if local_branch == remote_branch:
252 argv.append(local_branch)
253 else:
254 if not ffwd and local_branch:
255 argv.append('+%s:%s' % ( local_branch, remote_branch ))
256 else:
257 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 return git('remote', stderr=False, *args).splitlines()
267 def remote_url(name):
268 return config('remote.%s.url' % name)
270 def reset(to_unstage):
271 '''Use 'git reset' to unstage files from the index.'''
272 if not to_unstage:
273 return 'No files to reset.'
275 argv = [ 'reset', '--' ]
276 argv.extend(to_unstage)
278 return git(*argv)
280 def rev_list_range(start, end):
281 argv = [ 'rev-list', '--pretty=oneline', start, end ]
282 raw_revs = git(*argv).splitlines()
283 revs = []
284 for line in raw_revs:
285 match = REV_LIST_REGEX.match(line)
286 if match:
287 rev_id = match.group(1)
288 summary = match.group(2)
289 revs.append((rev_id, summary,) )
290 return revs
292 def show(sha1):
293 return git('show',sha1)
295 def show_cdup():
296 '''Returns a relative path to the git project root.'''
297 return git('rev-parse','--show-cdup')
299 def status():
300 '''RETURNS: A tuple of staged, unstaged and untracked files.
301 ( array(staged), array(unstaged), array(untracked) )'''
303 status_lines = git('status').splitlines()
305 unstaged_header_seen = False
306 untracked_header_seen = False
308 modified_header = '# Changed but not updated:'
309 modified_regex = re.compile('(#\tmodified:\W{3}'
310 + '|#\tnew file:\W{3}'
311 + '|#\tdeleted:\W{4})')
313 renamed_regex = re.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
315 untracked_header = '# Untracked files:'
316 untracked_regex = re.compile('#\t(.+)')
318 staged = []
319 unstaged = []
320 untracked = []
322 # Untracked files
323 for status_line in status_lines:
324 if untracked_header in status_line:
325 untracked_header_seen = True
326 continue
327 if not untracked_header_seen:
328 continue
329 match = untracked_regex.match(status_line)
330 if match:
331 filename = match.group(1)
332 untracked.append(filename)
334 # Staged, unstaged, and renamed files
335 for status_line in status_lines:
336 if modified_header in status_line:
337 unstaged_header_seen = True
338 continue
339 match = modified_regex.match(status_line)
340 if match:
341 tag = match.group(0)
342 filename = status_line.replace(tag, '')
343 if unstaged_header_seen:
344 unstaged.append(filename)
345 else:
346 staged.append(filename)
347 continue
348 # Renamed files
349 match = renamed_regex.match(status_line)
350 if match:
351 oldname = match.group(2)
352 newname = match.group(3)
353 staged.append(oldname)
354 staged.append(newname)
356 return( staged, unstaged, untracked )
358 def tag():
359 return git('tag').splitlines()