7 from cStringIO
import StringIO
9 def git(*args
,**kwargs
):
10 """This is a convenience wrapper around run_cmd that
11 sets things up so that commands are run in the canonical
12 'git command [options] [args]' form."""
13 cmd
= 'git %s' % args
[0]
14 return utils
.run_cmd(cmd
, *args
[1:], **kwargs
)
16 class GitCommand(object):
17 """This class wraps this module so that arbitrary git commands
18 can be dynamically called at runtime."""
19 def __init__(self
, module
):
21 def __getattr__(self
, name
):
22 if hasattr(self
.module
, name
):
23 return getattr(self
.module
, name
)
24 def git_cmd(*args
, **kwargs
):
25 return git(name
.replace('_','-'), *args
, **kwargs
)
28 # At import we replace this module with a GitCommand singleton.
29 gitcmd
= GitCommand(sys
.modules
[__name__
])
30 sys
.modules
[__name__
] = gitcmd
33 #+-------------------------------------------------------------------------
34 #+ A regex for matching the output of git(log|rev-list) --pretty=oneline
35 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
38 return ' '.join([ utils
.shell_quote(arg
) for arg
in argv
])
40 def add_or_remove(*to_process
):
41 """Invokes 'git add' to index the filenames in to_process that exist
42 and 'git rm' for those that do not exist."""
45 return 'No files to add or remove.'
50 for filename
in to_process
:
51 if os
.path
.exists(filename
):
52 to_add
.append(filename
)
54 output
= gitcmd
.add(verbose
=True, *to_add
)
56 if len(to_add
) == len(to_process
):
57 # to_process only contained unremoved files --
58 # short-circuit the removal checks
61 # Process files to remote
62 for filename
in to_process
:
63 if not os
.path
.exists(filename
):
64 to_remove
.append(filename
)
65 output
+ '\n\n' + gitcmd
.rm(*to_remove
)
67 def branch(name
=None, remote
=False, delete
=False):
69 return git('branch', name
, D
=True)
71 branches
= map(lambda x
: x
.lstrip('* '),
72 git('branch', r
=remote
).splitlines())
75 for branch
in branches
:
76 if branch
.endswith('/HEAD'):
78 remotes
.append(branch
)
82 def cherry_pick_list(revs
, **kwargs
):
83 """Cherry-picks each revision into the current branch.
84 Returns a list of command output strings (1 per cherry pick)"""
89 cherries
.append(gitcmd
.cherry_pick(rev
, **kwargs
))
90 return '\n'.join(cherries
)
92 def commit_with_msg(msg
, amend
=False):
93 """Creates a git commit."""
95 if not msg
.endswith('\n'):
98 # Sure, this is a potential "security risk," but if someone
99 # is trying to intercept/re-write commit messages on your system,
100 # then you probably have bigger problems to worry about.
101 tmpfile
= utils
.get_tmp_filename()
107 # Create the commit message file
108 file = open(tmpfile
, 'w')
113 output
= gitcmd
.commit(F
=tmpfile
, amend
=amend
)
116 return ('git commit -F %s --amend %s\n\n%s'
117 % ( tmpfile
, amend
, output
))
119 def create_branch(name
, base
, track
=False):
120 """Creates a branch starting from base. Pass track=True
121 to create a remote tracking branch."""
122 return git('branch', name
, base
, track
=track
)
124 def current_branch():
125 """Parses 'git branch' to find the current branch."""
126 branches
= git('branch').splitlines()
127 for branch
in branches
:
128 if branch
.startswith('* '):
129 return branch
.lstrip('* ')
130 return 'Detached HEAD'
132 def diff_helper(commit
=None,
136 with_diff_header
=False,
137 suppress_header
=True,
139 "Invokes git diff on a filepath."
143 argv
.append('%s^..%s' % (commit
, commit
))
147 if type(filename
) is list:
148 argv
.extend(filename
)
150 argv
.append(filename
)
157 unified
= defaults
.DIFF_CONTEXT
,
163 del_tag
= 'deleted file mode '
166 deleted
= cached
and not os
.path
.exists(filename
)
168 if not start
and '@@ ' in line
and ' @@' in line
:
170 if start
or(deleted
and del_tag
in line
):
171 output
.write(line
+ '\n')
175 elif not suppress_header
:
176 output
.write(line
+ '\n')
177 result
= output
.getvalue()
180 return('\n'.join(headers
), result
)
187 unified
=defaults
.DIFF_CONTEXT
,
192 unified
=defaults
.DIFF_CONTEXT
,
196 def format_patch_helper(*revs
):
197 """writes patches named by revs to the "patches" directory."""
200 for idx
, rev
in enumerate(revs
):
201 real_idx
= idx
+ num_patches
202 revarg
= '%s^..%s' % (rev
,rev
)
207 start_number
=real_idx
,
213 num_patches
+= output
[-1].count('\n')
214 return '\n'.join(output
)
216 def config_dict(local
=True):
220 argv
= ['--global', '--list' ]
221 return config_to_dict(
222 gitcmd
.config(*argv
).splitlines())
224 def config_set(key
=None, value
=None, local
=True):
225 if key
and value
is not None:
226 # git config category.key value
228 if type(value
) is bool:
229 # git uses "true" and "false"
230 strval
= strval
.lower()
232 argv
= [ key
, strval
]
234 argv
= [ '--global', key
, strval
]
235 return gitcmd
.config(*argv
)
237 msg
= "oops in git.config_set(key=%s,value=%s,local=%s"
238 raise Exception(msg
% (key
, value
, local
))
240 def config_to_dict(config_lines
):
241 """parses the lines from git config --list into a dictionary"""
244 for line
in config_lines
:
245 k
, v
= line
.split('=')
246 k
= k
.replace('.','_') # git -> model
247 if v
== 'true' or v
== 'false':
248 v
= bool(eval(v
.title()))
256 def log_helper(all
=False):
257 """Returns a pair of parallel arrays listing the revision sha1's
258 and commit summaries."""
261 regex
= REV_LIST_REGEX
262 output
= gitcmd
.log(pretty
='oneline', all
=all
)
263 for line
in output
.splitlines():
264 match
= regex
.match(line
)
266 revs
.append(match
.group(1))
267 summaries
.append(match
.group(2))
268 return( revs
, summaries
)
270 def parse_ls_tree(rev
):
271 """Returns a list of(mode, type, sha1, path) tuples."""
272 lines
= gitcmd
.ls_tree(rev
, r
=True).splitlines()
274 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
276 match
= regex
.match(line
)
278 mode
= match
.group(1)
279 objtype
= match
.group(2)
280 sha1
= match
.group(3)
281 filename
= match
.group(4)
282 output
.append((mode
, objtype
, sha1
, filename
,) )
285 def push_helper(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
287 branch_arg
= '%s:%s' % ( local_branch
, remote_branch
)
289 branch_arg
= '+%s:%s' % ( local_branch
, remote_branch
)
290 return gitcmd
.push(remote
, branch_arg
, with_status
=True, tags
=tags
)
292 def remote_url(name
):
293 return gitcmd
.config('remote.%s.url' % name
, get
=True)
295 def rev_list_range(start
, end
):
296 range = '%s..%s' % ( start
, end
)
297 raw_revs
= gitcmd
.rev_list(range, pretty
='oneline')
298 return parse_rev_list(raw_revs
)
300 def parse_rev_list(raw_revs
):
302 for line
in raw_revs
.splitlines():
303 match
= REV_LIST_REGEX
.match(line
)
305 rev_id
= match
.group(1)
306 summary
= match
.group(2)
307 revs
.append((rev_id
, summary
,) )
311 """RETURNS: A tuple of staged, unstaged and untracked file lists."""
313 MODIFIED_TAG
= '# Changed but not updated:'
314 UNTRACKED_TAG
= '# Untracked files:'
316 RGX_RENAMED
= re
.compile(
321 RGX_MODIFIED
= re
.compile(
335 current_dest
= staged
337 for status_line
in gitcmd
.status().splitlines():
338 if status_line
== MODIFIED_TAG
:
340 current_dest
= unstaged
343 elif status_line
== UNTRACKED_TAG
:
344 mode
= UNTRACKED_MODE
345 current_dest
= untracked
348 # Staged/unstaged modified/renamed/deleted files
349 if mode
== STAGED_MODE
or mode
== UNSTAGED_MODE
:
350 match
= RGX_MODIFIED
.match(status_line
)
353 filename
= status_line
.replace(tag
, '')
354 current_dest
.append(filename
)
356 match
= RGX_RENAMED
.match(status_line
)
358 oldname
= match
.group(2)
359 newname
= match
.group(3)
360 current_dest
.append(oldname
)
361 current_dest
.append(newname
)
364 elif mode
is UNTRACKED_MODE
:
365 if status_line
.startswith('#\t'):
366 current_dest
.append(status_line
[2:])
368 return( staged
, unstaged
, untracked
)