Avoid raising an exception when a child cannot be started.
[ugit.git] / ugitlibs / git.py
blob4d8afa8722343f1768e1b64ca0934b39a93cdd97
1 '''TODO: "import stgit"'''
2 import os
3 import re
4 import types
6 import defaults
7 import utils
8 from cStringIO import StringIO
10 # A regex for matching the output of git(log|rev-list) --pretty=oneline
11 REV_LIST_REGEX = re.compile('([0-9a-f]+)\W(.*)')
13 def quote(argv):
14 return ' '.join([ utils.shell_quote(arg) for arg in argv ])
16 def git(*args,**kwargs):
17 return utils.run_cmd('git', *args, **kwargs)
19 def add(to_add):
20 '''Invokes 'git add' to index the filenames in to_add.'''
21 if not to_add: return 'No files to add.'
22 return git('add', *to_add)
24 def add_or_remove(to_process):
25 '''Invokes 'git add' to index the filenames in to_process that exist
26 and 'git rm' for those that do not exist.'''
28 if not to_process:
29 return 'No files to add or remove.'
31 to_add = []
32 to_remove = []
33 output = ''
35 for filename in to_process:
36 if os.path.exists(filename):
37 to_add.append(filename)
39 add(to_add)
41 if len(to_add) == len(to_process):
42 # to_process only contained unremoved files --
43 # short-circuit the removal checks
44 return
46 # Process files to remote
47 for filename in to_process:
48 if not os.path.exists(filename):
49 to_remove.append(filename)
50 git('rm',*to_remove)
52 def apply(filename, indexonly=True, reverse=False):
53 argv = ['apply']
54 if reverse: argv.append('--reverse')
55 if indexonly: argv.extend(['--index', '--cached'])
56 argv.append(filename)
57 return git(*argv)
59 def branch(name=None, remote=False, delete=False):
60 if delete and name:
61 return git('branch', '-D', name)
62 else:
63 argv = ['branch']
64 if remote: argv.append('-r')
66 branches = git(*argv).splitlines()
67 return map(lambda(x): x.lstrip('* '), branches)
69 def cat_file(objtype, sha1):
70 return git('cat-file', objtype, sha1, raw=True)
72 def cherry_pick(revs, commit=False):
73 '''Cherry-picks each revision into the current branch.'''
74 if not revs:
75 return 'No revision selected.'
76 argv = [ 'cherry-pick' ]
77 if not commit: argv.append('-n')
79 cherries = []
80 for rev in revs:
81 new_argv = argv + [rev]
82 cherries.append(git(*new_argv))
84 return os.linesep.join(cherries)
86 def checkout(rev):
87 return git('checkout', rev)
89 def commit(msg, amend=False):
90 '''Creates a git commit.'''
92 if not msg.endswith(os.linesep):
93 msg += os.linesep
95 # Sure, this is a potential "security risk," but if someone
96 # is trying to intercept/re-write commit messages on your system,
97 # then you probably have bigger problems to worry about.
98 tmpfile = utils.get_tmp_filename()
99 argv = [ 'commit', '-F', tmpfile ]
100 if amend:
101 argv.append('--amend')
103 # Create the commit message file
104 file = open(tmpfile, 'w')
105 file.write(msg)
106 file.close()
108 # Run 'git commit'
109 output = git(*argv)
110 os.unlink(tmpfile)
112 return quote(argv) + os.linesep*2 + output
114 def create_branch(name, base, track=False):
115 '''Creates a branch starting from base. Pass track=True
116 to create a remote tracking branch.'''
117 if track:
118 return git('branch', '--track', name, base)
119 else:
120 return git('branch', name, base)
122 def current_branch():
123 '''Parses 'git branch' to find the current branch.'''
124 branches = git('branch').splitlines()
125 for branch in branches:
126 if branch.startswith('* '):
127 return branch.lstrip('* ')
128 return 'Detached HEAD'
130 def diff(commit=None,filename=None, color=False,
131 cached=True, with_diff_header=False,
132 suppress_header=True, reverse=False):
133 "Invokes git diff on a filepath."
135 argv = [ 'diff', '--unified='+str(defaults.DIFF_CONTEXT), '--patch-with-raw']
136 if reverse: argv.append('-R')
137 if color: argv.append('--color')
138 if cached: argv.append('--cached')
140 deleted = cached and not os.path.exists(filename)
142 if filename:
143 argv.append('--')
144 argv.append(filename)
146 if commit:
147 argv.append('%s^..%s' % (commit,commit))
149 diff = git(*argv)
150 diff_lines = diff.splitlines()
152 output = StringIO()
153 start = False
154 del_tag = 'deleted file mode '
156 headers = []
157 for line in diff_lines:
158 if not start and '@@ ' in line and ' @@' in line:
159 start = True
160 if start or(deleted and del_tag in line):
161 output.write(line)
162 output.write(os.linesep)
163 else:
164 if with_diff_header:
165 headers.append(line)
166 elif not suppress_header:
167 output.write(line)
168 output.write(os.linesep)
170 result = output.getvalue()
171 output.close()
173 if with_diff_header:
174 return(os.linesep.join(headers), result)
175 else:
176 return result
178 def diff_stat():
179 return git('diff','--stat','HEAD^')
181 def format_patch(revs):
182 '''writes patches named by revs to the "patches" directory.'''
183 num_patches = 1
184 output = []
185 argv = ['format-patch','--thread','--patch-with-stat', '-o','patches']
186 if len(revs) > 1:
187 argv.append('-n')
188 for idx, rev in enumerate(revs):
189 real_idx = str(idx + num_patches)
190 new_argv = argv + ['--start-number', real_idx,
191 '%s^..%s'%(rev,rev)]
192 output.append(git(*new_argv))
193 num_patches += output[-1].count(os.linesep)
194 return os.linesep.join(output)
196 def config(key, value=None, local=True):
197 argv = ['config']
198 if not local:
199 argv.append('--global')
200 if value is None:
201 argv.append('--get')
202 argv.append(key)
203 else:
204 argv.append(key)
205 argv.append(value)
206 return git(*argv)
208 def log(oneline=True, all=False):
209 '''Returns a pair of parallel arrays listing the revision sha1's
210 and commit summaries.'''
211 argv = [ 'log' ]
212 if oneline:
213 argv.append('--pretty=oneline')
214 if all:
215 argv.append('--all')
216 revs = []
217 summaries = []
218 regex = REV_LIST_REGEX
219 output = git(*argv)
220 for line in output.splitlines():
221 match = regex.match(line)
222 if match:
223 revs.append(match.group(1))
224 summaries.append(match.group(2))
225 return( revs, summaries )
227 def ls_files():
228 return git('ls-files').splitlines()
230 def ls_tree(rev):
231 '''Returns a list of(mode, type, sha1, path) tuples.'''
232 lines = git('ls-tree', '-r', rev).splitlines()
233 output = []
234 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
235 for line in lines:
236 match = regex.match(line)
237 if match:
238 mode = match.group(1)
239 objtype = match.group(2)
240 sha1 = match.group(3)
241 filename = match.group(4)
242 output.append((mode, objtype, sha1, filename,) )
243 return output
245 def push(remote, local_branch, remote_branch, ffwd=True, tags=False):
246 argv = ['push']
247 if tags:
248 argv.append('--tags')
249 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 ))
257 return git(with_status=True, *argv)
259 def rebase(newbase):
260 if not newbase: return
261 return git('rebase', newbase)
263 def remote(*args):
264 return git('remote',*args).splitlines()
266 def remote_url(name):
267 return config('remote.%s.url' % name)
269 def reset(to_unstage):
270 '''Use 'git reset' to unstage files from the index.'''
271 if not to_unstage:
272 return 'No files to reset.'
274 argv = [ 'reset', '--' ]
275 argv.extend(to_unstage)
277 return git(*argv)
279 def rev_list_range(start, end):
280 argv = [ 'rev-list', '--pretty=oneline', start, end ]
281 raw_revs = git(*argv).splitlines()
282 revs = []
283 for line in raw_revs:
284 match = REV_LIST_REGEX.match(line)
285 if match:
286 rev_id = match.group(1)
287 summary = match.group(2)
288 revs.append((rev_id, summary,) )
289 return revs
291 def show(sha1):
292 return git('show',sha1)
294 def show_cdup():
295 '''Returns a relative path to the git project root.'''
296 return git('rev-parse','--show-cdup')
298 def status():
299 '''RETURNS: A tuple of staged, unstaged and untracked files.
300 ( array(staged), array(unstaged), array(untracked) )'''
302 status_lines = git('status').splitlines()
304 unstaged_header_seen = False
305 untracked_header_seen = False
307 modified_header = '# Changed but not updated:'
308 modified_regex = re.compile('(#\tmodified:\W{3}'
309 + '|#\tnew file:\W{3}'
310 + '|#\tdeleted:\W{4})')
312 renamed_regex = re.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
314 untracked_header = '# Untracked files:'
315 untracked_regex = re.compile('#\t(.+)')
317 staged = []
318 unstaged = []
319 untracked = []
321 # Untracked files
322 for status_line in status_lines:
323 if untracked_header in status_line:
324 untracked_header_seen = True
325 continue
326 if not untracked_header_seen:
327 continue
328 match = untracked_regex.match(status_line)
329 if match:
330 filename = match.group(1)
331 untracked.append(filename)
333 # Staged, unstaged, and renamed files
334 for status_line in status_lines:
335 if modified_header in status_line:
336 unstaged_header_seen = True
337 continue
338 match = modified_regex.match(status_line)
339 if match:
340 tag = match.group(0)
341 filename = status_line.replace(tag, '')
342 if unstaged_header_seen:
343 unstaged.append(filename)
344 else:
345 staged.append(filename)
346 continue
347 # Renamed files
348 match = renamed_regex.match(status_line)
349 if match:
350 oldname = match.group(2)
351 newname = match.group(3)
352 staged.append(oldname)
353 staged.append(newname)
355 return( staged, unstaged, untracked )
357 def tag():
358 return git('tag').splitlines()