gui: allow torn widgets to be on top
[git-cola.git] / ugitlibs / git.py
blob4199832d84e31afefa18fc0fbd3cf9445e07d77b
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 gitcmd = 'git %s' % args[0]
17 return utils.run_cmd(gitcmd, *args[1:], **kwargs)
19 def add(to_add, verbose=True):
20 '''Invokes 'git add' to index the filenames in to_add.'''
21 if not to_add:
22 return 'No files to add.'
23 return git('add', verbose=verbose, *to_add)
25 def add_or_remove(to_process):
26 '''Invokes 'git add' to index the filenames in to_process that exist
27 and 'git rm' for those that do not exist.'''
29 if not to_process:
30 return 'No files to add or remove.'
32 to_add = []
33 to_remove = []
35 for filename in to_process:
36 if os.path.exists(filename):
37 to_add.append(filename)
39 output = 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 output
46 # Process files to remote
47 for filename in to_process:
48 if not os.path.exists(filename):
49 to_remove.append(filename)
50 output + '\n\n' + git('rm',*to_remove)
52 def apply(filename, indexonly=True, reverse=False):
53 kwargs = {}
54 if reverse:
55 kwargs['reverse'] = True
56 if indexonly:
57 kwargs['index'] = True
58 kwargs['cached'] = True
59 argv = ['apply', filename]
60 return git(*argv, **kwargs)
62 def branch(name=None, remote=False, delete=False):
63 if delete and name:
64 return git('branch', name, D=True)
65 else:
66 branches = map(lambda x: x.lstrip('* '),
67 git('branch', r=remote).splitlines())
68 if remote:
69 remotes = []
70 for branch in branches:
71 if branch.endswith('/HEAD'):
72 continue
73 remotes.append(branch)
74 return remotes
75 return branches
77 def cat_file(objtype, sha1):
78 return git('cat-file', objtype, sha1, raw=True)
80 def cherry_pick(revs, commit=False):
81 """Cherry-picks each revision into the current branch.
82 Returns a list of command output strings (1 per cherry pick)"""
84 if not revs: return []
86 argv = [ 'cherry-pick' ]
87 kwargs = {}
88 if not commit:
89 kwargs['n'] = True
91 cherries = []
92 for rev in revs:
93 new_argv = argv + [rev]
94 cherries.append(git(*new_argv, **kwargs))
96 return '\n'.join(cherries)
98 def checkout(*args):
99 return git('checkout', *args)
101 def commit(msg, amend=False):
102 '''Creates a git commit.'''
104 if not msg.endswith('\n'):
105 msg += '\n'
107 # Sure, this is a potential "security risk," but if someone
108 # is trying to intercept/re-write commit messages on your system,
109 # then you probably have bigger problems to worry about.
110 tmpfile = utils.get_tmp_filename()
111 kwargs = {
112 'F': tmpfile,
113 'amend': amend,
116 # Create the commit message file
117 file = open(tmpfile, 'w')
118 file.write(msg)
119 file.close()
121 # Run 'git commit'
122 output = git('commit', F=tmpfile, amend=amend)
123 os.unlink(tmpfile)
125 return ('git commit -F %s --amend %s\n\n%s'
126 % ( tmpfile, amend, output ))
128 def create_branch(name, base, track=False):
129 """Creates a branch starting from base. Pass track=True
130 to create a remote tracking branch."""
131 return git('branch', name, base, track=track)
133 def current_branch():
134 '''Parses 'git branch' to find the current branch.'''
135 branches = git('branch').splitlines()
136 for branch in branches:
137 if branch.startswith('* '):
138 return branch.lstrip('* ')
139 return 'Detached HEAD'
141 def diff(commit=None,filename=None, color=False,
142 cached=True, with_diff_header=False,
143 suppress_header=True, reverse=False):
144 "Invokes git diff on a filepath."
146 argv = []
147 if commit:
148 argv.append('%s^..%s' % (commit, commit))
150 if filename:
151 argv.append('--')
152 if type(filename) is list:
153 argv.extend(filename)
154 else:
155 argv.append(filename)
157 kwargs = {
158 'patch-with-raw': True,
159 'unified': defaults.DIFF_CONTEXT,
162 diff = git('diff',
163 R=reverse,
164 color=color,
165 cached=cached,
166 *argv,
167 **kwargs)
169 diff_lines = diff.splitlines()
171 output = StringIO()
172 start = False
173 del_tag = 'deleted file mode '
175 headers = []
176 deleted = cached and not os.path.exists(filename)
177 for line in diff_lines:
178 if not start and '@@ ' in line and ' @@' in line:
179 start = True
180 if start or(deleted and del_tag in line):
181 output.write(line + '\n')
182 else:
183 if with_diff_header:
184 headers.append(line)
185 elif not suppress_header:
186 output.write(line + '\n')
187 result = output.getvalue()
188 output.close()
189 if with_diff_header:
190 return('\n'.join(headers), result)
191 else:
192 return result
194 def diffstat():
195 return git('diff', 'HEAD^',
196 unified=defaults.DIFF_CONTEXT,
197 stat=True)
199 def diffindex():
200 return git('diff',
201 unified=defaults.DIFF_CONTEXT,
202 stat=True,
203 cached=True)
205 def format_patch(revs):
206 '''writes patches named by revs to the "patches" directory.'''
207 num_patches = 1
208 output = []
209 kwargs = {
210 'o': 'patches',
211 'n': len(revs) > 1,
212 'thread': True,
213 'patch-with-stat': True,
215 for idx, rev in enumerate(revs):
216 real_idx = idx + num_patches
217 kwargs['start-number'] = real_idx
218 revarg = '%s^..%s'%(rev,rev)
219 output.append(git('format-patch', revarg, **kwargs))
220 num_patches += output[-1].count('\n')
221 return '\n'.join(output)
223 def config(key=None, value=None, local=False, asdict=False):
224 if key:
225 argv = ['config', key]
226 else:
227 argv = ['config']
229 kwargs = {
230 'global': local is False,
231 'get': key and value is None,
232 'list': asdict,
235 if asdict:
236 return config_to_dict(git('config', **kwargs).splitlines())
238 elif kwargs['get']:
239 return git('config', key, **kwargs)
241 elif key and value is not None:
242 # git config category.key value
243 strval = str(value)
244 if type(value) is bool:
245 # git uses "true" and "false"
246 strval = strval.lower()
247 return git('config', key, strval, **kwargs)
248 else:
249 msg = "oops in git.config(key=%s,value=%s,local=%s,asdict=%s"
250 raise Exception(msg % (key, value, local, asdict))
253 def config_to_dict(config_lines):
254 """parses the lines from git config --list into a dictionary"""
256 newdict = {}
257 for line in config_lines:
258 k, v = line.split('=')
259 k = k.replace('.','_') # git -> model
260 if v == 'true' or v == 'false':
261 v = bool(eval(v.title()))
262 try:
263 v = int(eval(v))
264 except:
265 pass
266 newdict[k]=v
267 return newdict
269 def log(oneline=True, all=False):
270 '''Returns a pair of parallel arrays listing the revision sha1's
271 and commit summaries.'''
272 kwargs = {}
273 if oneline:
274 kwargs['pretty'] = 'oneline'
275 revs = []
276 summaries = []
277 regex = REV_LIST_REGEX
278 output = git('log', all=all, **kwargs)
279 for line in output.splitlines():
280 match = regex.match(line)
281 if match:
282 revs.append(match.group(1))
283 summaries.append(match.group(2))
284 return( revs, summaries )
286 def ls_files():
287 """git ls-files as a list"""
288 return git('ls-files').splitlines()
290 def ls_tree(rev):
291 """Returns a list of(mode, type, sha1, path) tuples."""
292 lines = git('ls-tree', rev, r=True).splitlines()
293 output = []
294 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
295 for line in lines:
296 match = regex.match(line)
297 if match:
298 mode = match.group(1)
299 objtype = match.group(2)
300 sha1 = match.group(3)
301 filename = match.group(4)
302 output.append((mode, objtype, sha1, filename,) )
303 return output
305 def push(remote, local_branch, remote_branch, ffwd=True, tags=False):
306 if ffwd:
307 branch_arg = '%s:%s' % ( local_branch, remote_branch )
308 else:
309 branch_arg = '+%s:%s' % ( local_branch, remote_branch )
310 return git('push', remote, branch_arg, with_status=True, tags=tags)
312 def rebase(newbase):
313 if not newbase:
314 return 'No base branch specified to rebase.'
315 return git('rebase', newbase)
317 def remote(*args):
318 return git('remote', without_stderr=True, *args).splitlines()
320 def remote_url(name):
321 return config('remote.%s.url' % name, local=True)
323 def reset(to_unstage):
324 '''Use 'git reset' to unstage files from the index.'''
325 if not to_unstage:
326 return 'No files to reset.'
328 argv = [ 'reset', '--' ]
329 argv.extend(to_unstage)
330 return git(*argv)
332 def rev_list_range(start, end):
333 argv = [ 'rev-list', '--pretty=oneline', start, end ]
334 raw_revs = git(*argv).splitlines()
335 revs = []
336 for line in raw_revs:
337 match = REV_LIST_REGEX.match(line)
338 if match:
339 rev_id = match.group(1)
340 summary = match.group(2)
341 revs.append((rev_id, summary,) )
342 return revs
344 def show(sha1):
345 return git('show',sha1)
347 def show_cdup():
348 '''Returns a relative path to the git project root.'''
349 return git('rev-parse','--show-cdup')
351 def status():
352 '''RETURNS: A tuple of staged, unstaged and untracked files.
353 ( array(staged), array(unstaged), array(untracked) )'''
355 status_lines = git('status').splitlines()
357 unstaged_header_seen = False
358 untracked_header_seen = False
360 modified_header = '# Changed but not updated:'
361 modified_regex = re.compile('(#\tmodified:\s+'
362 '|#\tnew file:\s+'
363 '|#\tdeleted:\s+)')
365 renamed_regex = re.compile('(#\trenamed:\s+)(.*?)\s->\s(.*)')
367 untracked_header = '# Untracked files:'
368 untracked_regex = re.compile('#\t(.+)')
370 staged = []
371 unstaged = []
372 untracked = []
374 # Untracked files
375 for status_line in status_lines:
376 if untracked_header in status_line:
377 untracked_header_seen = True
378 continue
379 if not untracked_header_seen:
380 continue
381 match = untracked_regex.match(status_line)
382 if match:
383 filename = match.group(1)
384 untracked.append(filename)
386 # Staged, unstaged, and renamed files
387 for status_line in status_lines:
388 if modified_header in status_line:
389 unstaged_header_seen = True
390 continue
391 match = modified_regex.match(status_line)
392 if match:
393 tag = match.group(0)
394 filename = status_line.replace(tag, '')
395 if unstaged_header_seen:
396 unstaged.append(filename)
397 else:
398 staged.append(filename)
399 continue
400 # Renamed files
401 match = renamed_regex.match(status_line)
402 if match:
403 oldname = match.group(2)
404 newname = match.group(3)
405 staged.append(oldname)
406 staged.append(newname)
408 return( staged, unstaged, untracked )
410 def tag():
411 return git('tag').splitlines()