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
):
24 # This creates git.foo() methods dynamically for each of the
25 # following names at import-time.
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
)
69 def git_cmd(*args
, **kwargs
):
70 return git(name
.replace('_','-'), *args
, **kwargs
)
71 setattr(self
, name
, 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(.*)')
83 return ' '.join([ utils
.shell_quote(arg
) for arg
in argv
])
87 output
= gitcmd
.read_tree("HEAD", reset
=True, u
=True, v
=True)
89 merge_head
= git_repo_path('MERGE_HEAD')
90 if os
.path
.exists(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."""
103 return 'No files to add or 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
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())
130 for branch
in branches
:
131 if branch
.endswith('/HEAD'):
133 remotes
.append(branch
)
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)"""
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'):
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()
160 # Create the commit message file
161 file = open(tmpfile
, 'w')
166 output
= gitcmd
.commit(F
=tmpfile
, amend
=amend
)
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,
190 with_diff_header
=False,
191 suppress_header
=True,
193 "Invokes git diff on a filepath."
197 argv
.append('%s^..%s' % (commit
, commit
))
201 if type(filename
) is list:
202 argv
.extend(filename
)
204 argv
.append(filename
)
211 unified
= defaults
.DIFF_CONTEXT
,
217 del_tag
= 'deleted file mode '
220 deleted
= cached
and not os
.path
.exists(filename
)
222 if not start
and '@@ ' in line
and ' @@' in line
:
224 if start
or(deleted
and del_tag
in line
):
225 output
.write(line
+ '\n')
229 elif not suppress_header
:
230 output
.write(line
+ '\n')
231 result
= output
.getvalue()
234 return('\n'.join(headers
), result
)
241 unified
=defaults
.DIFF_CONTEXT
,
246 unified
=defaults
.DIFF_CONTEXT
,
250 def format_patch_helper(*revs
):
251 """writes patches named by revs to the "patches" directory."""
254 for idx
, rev
in enumerate(revs
):
255 real_idx
= idx
+ num_patches
256 revarg
= '%s^..%s' % (rev
,rev
)
261 start_number
=real_idx
,
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):
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
285 if type(value
) is bool:
286 # git uses "true" and "false"
287 strval
= strval
.lower()
289 argv
= [ key
, strval
]
291 argv
= [ '--global', key
, strval
]
292 return gitcmd
.config(*argv
)
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"""
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()))
313 def log_helper(all
=False):
314 """Returns a pair of parallel arrays listing the revision sha1's
315 and commit 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
)
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()
331 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
333 match
= regex
.match(line
)
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
,) )
342 def push_helper(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
344 branch_arg
= '%s:%s' % ( local_branch
, remote_branch
)
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
):
369 def reset_helper(*args
, **kwargs
):
370 return gitcmd
.reset('--', *args
, **kwargs
)
372 def parse_rev_list(raw_revs
):
374 for line
in raw_revs
.splitlines():
375 match
= REV_LIST_REGEX
.match(line
)
377 rev_id
= match
.group(1)
378 summary
= match
.group(2)
379 revs
.append((rev_id
, summary
,) )
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(
393 RGX_MODIFIED
= re
.compile(
407 current_dest
= staged
409 for status_line
in gitcmd
.status().splitlines():
410 if status_line
== MODIFIED_TAG
:
412 current_dest
= unstaged
415 elif status_line
== UNTRACKED_TAG
:
416 mode
= UNTRACKED_MODE
417 current_dest
= untracked
420 # Staged/unstaged modified/renamed/deleted files
421 if mode
== STAGED_MODE
or mode
== UNSTAGED_MODE
:
422 match
= RGX_MODIFIED
.match(status_line
)
425 filename
= status_line
.replace(tag
, '')
426 current_dest
.append(filename
)
428 match
= RGX_RENAMED
.match(status_line
)
430 oldname
= match
.group(2)
431 newname
= match
.group(3)
432 current_dest
.append(oldname
)
433 current_dest
.append(newname
)
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()