search: don't automatically search when the user types
[git-cola.git] / ugit / git.py
blob106946fd20c834ddbf706f65e68240816b7e7256
1 import os
2 import re
3 import sys
4 import types
5 import utils
6 import defaults
7 from cStringIO import StringIO
9 # union of functions in this file and dynamic functions
10 # defined in the git command string list below
11 def git(*args,**kwargs):
12 """This is a convenience wrapper around utils.run_cmd that
13 sets things up so that commands are run in the canonical
14 'git command [options] [args]' form."""
15 cmd = 'git %s' % args[0]
16 return utils.run_cmd(cmd, *args[1:], **kwargs)
18 class GitCommand(object):
19 """This class wraps this module so that arbitrary git commands
20 can be dynamically called at runtime."""
21 def __init__(self, module):
22 self.module = module
23 self.commands = {}
24 # This creates git.foo() methods dynamically for each of the
25 # following names at import-time.
26 for cmd in """
27 add
28 apply
29 branch
30 checkout
31 cherry_pick
32 commit
33 diff
34 fetch
35 format_patch
36 grep
37 log
38 ls_tree
39 merge
40 pull
41 push
42 rebase
43 remote
44 reset
45 read_tree
46 rev_list
48 show
49 status
50 tag
51 """.split(): getattr(self, cmd)
53 def setup_commands(self):
54 # Import the functions from the module
55 for name, val in self.module.__dict__.iteritems():
56 if type(val) is types.FunctionType:
57 setattr(self, name, val)
58 # Import dynamic functions and those from the module
59 # functions into self.commands
60 for name, val in self.__dict__.iteritems():
61 if type(val) is types.FunctionType:
62 self.commands[name] = val
64 def __getattr__(self, name):
65 if hasattr(self.module, name):
66 value = getattr(self.module, name)
67 setattr(self, name, value)
68 return value
69 def git_cmd(*args, **kwargs):
70 return git(name.replace('_','-'), *args, **kwargs)
71 setattr(self, name, git_cmd)
72 return git_cmd
74 # core git wrapper for use in this module
75 gitcmd = GitCommand(sys.modules[__name__])
76 sys.modules[__name__] = gitcmd
78 #+-------------------------------------------------------------------------
79 #+ A regex for matching the output of git(log|rev-list) --pretty=oneline
80 REV_LIST_REGEX = re.compile('([0-9a-f]+)\W(.*)')
82 def quote(argv):
83 return ' '.join([ utils.shell_quote(arg) for arg in argv ])
85 def abort_merge():
86 # Reset the worktree
87 output = gitcmd.read_tree("HEAD", reset=True, u=True, v=True)
88 # remove MERGE_HEAD
89 merge_head = git_repo_path('MERGE_HEAD')
90 if os.path.exists(merge_head):
91 os.unlink(merge_head)
92 # remove MERGE_MESSAGE, etc.
93 merge_msg_path = get_merge_message_path()
94 while merge_msg_path is not None:
95 os.unlink(merge_msg_path)
96 merge_msg_path = get_merge_message_path()
98 def add_or_remove(*to_process):
99 """Invokes 'git add' to index the filenames in to_process that exist
100 and 'git rm' for those that do not exist."""
102 if not to_process:
103 return 'No files to add or remove.'
105 to_add = []
106 to_remove = []
108 for filename in to_process:
109 if os.path.exists(filename):
110 to_add.append(filename)
112 output = gitcmd.add(verbose=True, *to_add)
114 if len(to_add) == len(to_process):
115 # to_process only contained unremoved files --
116 # short-circuit the removal checks
117 return output
119 # Process files to remote
120 for filename in to_process:
121 if not os.path.exists(filename):
122 to_remove.append(filename)
123 output + '\n\n' + gitcmd.rm(*to_remove)
125 def branch_list(remote=False):
126 branches = map(lambda x: x.lstrip('* '),
127 gitcmd.branch(r=remote).splitlines())
128 if remote:
129 remotes = []
130 for branch in branches:
131 if branch.endswith('/HEAD'):
132 continue
133 remotes.append(branch)
134 return remotes
135 return branches
137 def cherry_pick_list(revs, **kwargs):
138 """Cherry-picks each revision into the current branch.
139 Returns a list of command output strings (1 per cherry pick)"""
140 if not revs:
141 return []
142 cherries = []
143 for rev in revs:
144 cherries.append(gitcmd.cherry_pick(rev, **kwargs))
145 return '\n'.join(cherries)
147 def commit_with_msg(msg, amend=False):
148 """Creates a git commit."""
150 if not msg.endswith('\n'):
151 msg += '\n'
152 # Sure, this is a potential "security risk," but if someone
153 # is trying to intercept/re-write commit messages on your system,
154 # then you probably have bigger problems to worry about.
155 tmpfile = utils.get_tmp_filename()
156 kwargs = {
157 'F': tmpfile,
158 'amend': amend,
160 # Create the commit message file
161 file = open(tmpfile, 'w')
162 file.write(msg)
163 file.close()
165 # Run 'git commit'
166 output = gitcmd.commit(F=tmpfile, amend=amend)
167 os.unlink(tmpfile)
169 return ('git commit -F %s --amend %s\n\n%s'
170 % ( tmpfile, amend, output ))
172 def create_branch(name, base, track=False):
173 """Creates a branch starting from base. Pass track=True
174 to create a remote tracking branch."""
175 return gitcmd.branch(name, base, track=track)
177 def current_branch():
178 """Parses 'git branch' to find the current branch."""
180 branches = gitcmd.branch().splitlines()
181 for branch in branches:
182 if branch.startswith('* '):
183 return branch.lstrip('* ')
184 return 'Detached HEAD'
186 def diff_helper(commit=None,
187 filename=None,
188 color=False,
189 cached=True,
190 with_diff_header=False,
191 suppress_header=True,
192 reverse=False):
193 "Invokes git diff on a filepath."
195 argv = []
196 if commit:
197 argv.append('%s^..%s' % (commit, commit))
199 if filename:
200 argv.append('--')
201 if type(filename) is list:
202 argv.extend(filename)
203 else:
204 argv.append(filename)
206 diff = gitcmd.diff(
207 R=reverse,
208 color=color,
209 cached=cached,
210 patch_with_raw=True,
211 unified = defaults.DIFF_CONTEXT,
212 *argv
213 ).splitlines()
215 output = StringIO()
216 start = False
217 del_tag = 'deleted file mode '
219 headers = []
220 deleted = cached and not os.path.exists(filename)
221 for line in diff:
222 if not start and '@@ ' in line and ' @@' in line:
223 start = True
224 if start or(deleted and del_tag in line):
225 output.write(line + '\n')
226 else:
227 if with_diff_header:
228 headers.append(line)
229 elif not suppress_header:
230 output.write(line + '\n')
231 result = output.getvalue()
232 output.close()
233 if with_diff_header:
234 return('\n'.join(headers), result)
235 else:
236 return result
238 def diffstat():
239 return gitcmd.diff(
240 'HEAD^',
241 unified=defaults.DIFF_CONTEXT,
242 stat=True)
244 def diffindex():
245 return gitcmd.diff(
246 unified=defaults.DIFF_CONTEXT,
247 stat=True,
248 cached=True)
250 def format_patch_helper(*revs):
251 """writes patches named by revs to the "patches" directory."""
252 num_patches = 1
253 output = []
254 for idx, rev in enumerate(revs):
255 real_idx = idx + num_patches
256 revarg = '%s^..%s' % (rev,rev)
257 output.append(
258 gitcmd.format_patch(
259 revarg,
260 o='patches',
261 start_number=real_idx,
262 n=len(revs) > 1,
263 thread=True,
264 patch_with_stat=True
267 num_patches += output[-1].count('\n')
268 return '\n'.join(output)
270 def get_merge_message():
271 return gitcmd.fmt_merge_msg('--file', git_repo_path('FETCH_HEAD'))
273 def config_dict(local=True):
274 if local:
275 argv = [ '--list' ]
276 else:
277 argv = ['--global', '--list' ]
278 return config_to_dict(
279 gitcmd.config(*argv).splitlines())
281 def config_set(key=None, value=None, local=True):
282 if key and value is not None:
283 # git config category.key value
284 strval = str(value)
285 if type(value) is bool:
286 # git uses "true" and "false"
287 strval = strval.lower()
288 if local:
289 argv = [ key, strval ]
290 else:
291 argv = [ '--global', key, strval ]
292 return gitcmd.config(*argv)
293 else:
294 msg = "oops in git.config_set(key=%s,value=%s,local=%s"
295 raise Exception(msg % (key, value, local))
297 def config_to_dict(config_lines):
298 """parses the lines from git config --list into a dictionary"""
300 newdict = {}
301 for line in config_lines:
302 k, v = line.split('=')
303 k = k.replace('.','_') # git -> model
304 if v == 'true' or v == 'false':
305 v = bool(eval(v.title()))
306 try:
307 v = int(eval(v))
308 except:
309 pass
310 newdict[k]=v
311 return newdict
313 def log_helper(all=False):
314 """Returns a pair of parallel arrays listing the revision sha1's
315 and commit summaries."""
316 revs = []
317 summaries = []
318 regex = REV_LIST_REGEX
319 output = gitcmd.log(pretty='oneline', all=all)
320 for line in output.splitlines():
321 match = regex.match(line)
322 if match:
323 revs.append(match.group(1))
324 summaries.append(match.group(2))
325 return( revs, summaries )
327 def parse_ls_tree(rev):
328 """Returns a list of(mode, type, sha1, path) tuples."""
329 lines = gitcmd.ls_tree(rev, r=True).splitlines()
330 output = []
331 regex = re.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
332 for line in lines:
333 match = regex.match(line)
334 if match:
335 mode = match.group(1)
336 objtype = match.group(2)
337 sha1 = match.group(3)
338 filename = match.group(4)
339 output.append((mode, objtype, sha1, filename,) )
340 return output
342 def push_helper(remote, local_branch, remote_branch, ffwd=True, tags=False):
343 if ffwd:
344 branch_arg = '%s:%s' % ( local_branch, remote_branch )
345 else:
346 branch_arg = '+%s:%s' % ( local_branch, remote_branch )
347 return gitcmd.push(remote, branch_arg, with_status=True, tags=tags)
349 def remote_url(name):
350 return gitcmd.config('remote.%s.url' % name, get=True)
352 def rev_list_range(start, end):
353 range = '%s..%s' % ( start, end )
354 raw_revs = gitcmd.rev_list(range, pretty='oneline')
355 return parse_rev_list(raw_revs)
357 def git_repo_path(*subpaths):
358 paths = [ gitcmd.rev_parse(git_dir=True) ]
359 paths.extend(subpaths)
360 return os.path.realpath(os.path.join(*paths))
362 def get_merge_message_path():
363 for file in ('MERGE_MSG', 'SQUASH_MSG'):
364 path = git_repo_path(file)
365 if os.path.exists(path):
366 return path
367 return None
369 def reset_helper(*args, **kwargs):
370 return gitcmd.reset('--', *args, **kwargs)
372 def parse_rev_list(raw_revs):
373 revs = []
374 for line in raw_revs.splitlines():
375 match = REV_LIST_REGEX.match(line)
376 if match:
377 rev_id = match.group(1)
378 summary = match.group(2)
379 revs.append((rev_id, summary,) )
380 return revs
382 def parse_status():
383 """RETURNS: A tuple of staged, unstaged and untracked file lists."""
385 MODIFIED_TAG = '# Changed but not updated:'
386 UNTRACKED_TAG = '# Untracked files:'
388 RGX_RENAMED = re.compile(
389 '(#\trenamed:\s+)'
390 '(.*?)\s->\s(.*)'
393 RGX_MODIFIED = re.compile(
394 '(#\tmodified:\s+'
395 '|#\tnew file:\s+'
396 '|#\tdeleted:\s+)'
398 staged = []
399 unstaged = []
400 untracked = []
402 STAGED_MODE = 0
403 UNSTAGED_MODE = 1
404 UNTRACKED_MODE = 2
406 mode = STAGED_MODE
407 current_dest = staged
409 for status_line in gitcmd.status().splitlines():
410 if status_line == MODIFIED_TAG:
411 mode = UNSTAGED_MODE
412 current_dest = unstaged
413 continue
415 elif status_line == UNTRACKED_TAG:
416 mode = UNTRACKED_MODE
417 current_dest = untracked
418 continue
420 # Staged/unstaged modified/renamed/deleted files
421 if mode == STAGED_MODE or mode == UNSTAGED_MODE:
422 match = RGX_MODIFIED.match(status_line)
423 if match:
424 tag = match.group(0)
425 filename = status_line.replace(tag, '')
426 current_dest.append(filename)
427 continue
428 match = RGX_RENAMED.match(status_line)
429 if match:
430 oldname = match.group(2)
431 newname = match.group(3)
432 current_dest.append(oldname)
433 current_dest.append(newname)
434 continue
435 # Untracked files
436 elif mode is UNTRACKED_MODE:
437 if status_line.startswith('#\t'):
438 current_dest.append(status_line[2:])
440 return( staged, unstaged, untracked )
442 # Must be executed after all functions are defined
443 gitcmd.setup_commands()