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
])
86 return gitcmd
.read_tree("HEAD", reset
=True, u
=True, v
=True)
88 def add_or_remove(*to_process
):
89 """Invokes 'git add' to index the filenames in to_process that exist
90 and 'git rm' for those that do not exist."""
93 return 'No files to add or remove.'
98 for filename
in to_process
:
99 if os
.path
.exists(filename
):
100 to_add
.append(filename
)
102 output
= gitcmd
.add(verbose
=True, *to_add
)
104 if len(to_add
) == len(to_process
):
105 # to_process only contained unremoved files --
106 # short-circuit the removal checks
109 # Process files to remote
110 for filename
in to_process
:
111 if not os
.path
.exists(filename
):
112 to_remove
.append(filename
)
113 output
+ '\n\n' + gitcmd
.rm(*to_remove
)
115 def branch_list(remote
=False):
116 branches
= map(lambda x
: x
.lstrip('* '),
117 gitcmd
.branch(r
=remote
).splitlines())
120 for branch
in branches
:
121 if branch
.endswith('/HEAD'):
123 remotes
.append(branch
)
127 def cherry_pick_list(revs
, **kwargs
):
128 """Cherry-picks each revision into the current branch.
129 Returns a list of command output strings (1 per cherry pick)"""
134 cherries
.append(gitcmd
.cherry_pick(rev
, **kwargs
))
135 return '\n'.join(cherries
)
137 def commit_with_msg(msg
, amend
=False):
138 """Creates a git commit."""
140 if not msg
.endswith('\n'):
142 # Sure, this is a potential "security risk," but if someone
143 # is trying to intercept/re-write commit messages on your system,
144 # then you probably have bigger problems to worry about.
145 tmpfile
= utils
.get_tmp_filename()
150 # Create the commit message file
151 file = open(tmpfile
, 'w')
156 output
= gitcmd
.commit(F
=tmpfile
, amend
=amend
)
159 return ('git commit -F %s --amend %s\n\n%s'
160 % ( tmpfile
, amend
, output
))
162 def create_branch(name
, base
, track
=False):
163 """Creates a branch starting from base. Pass track=True
164 to create a remote tracking branch."""
165 return gitcmd
.branch(name
, base
, track
=track
)
167 def current_branch():
168 """Parses 'git branch' to find the current branch."""
170 branches
= gitcmd
.branch().splitlines()
171 for branch
in branches
:
172 if branch
.startswith('* '):
173 return branch
.lstrip('* ')
174 return 'Detached HEAD'
176 def diff_helper(commit
=None,
180 with_diff_header
=False,
181 suppress_header
=True,
183 "Invokes git diff on a filepath."
187 argv
.append('%s^..%s' % (commit
, commit
))
191 if type(filename
) is list:
192 argv
.extend(filename
)
194 argv
.append(filename
)
201 unified
= defaults
.DIFF_CONTEXT
,
207 del_tag
= 'deleted file mode '
210 deleted
= cached
and not os
.path
.exists(filename
)
212 if not start
and '@@ ' in line
and ' @@' in line
:
214 if start
or(deleted
and del_tag
in line
):
215 output
.write(line
+ '\n')
219 elif not suppress_header
:
220 output
.write(line
+ '\n')
221 result
= output
.getvalue()
224 return('\n'.join(headers
), result
)
231 unified
=defaults
.DIFF_CONTEXT
,
236 unified
=defaults
.DIFF_CONTEXT
,
240 def format_patch_helper(*revs
):
241 """writes patches named by revs to the "patches" directory."""
244 for idx
, rev
in enumerate(revs
):
245 real_idx
= idx
+ num_patches
246 revarg
= '%s^..%s' % (rev
,rev
)
251 start_number
=real_idx
,
257 num_patches
+= output
[-1].count('\n')
258 return '\n'.join(output
)
261 return os
.path
.join('.git', name
)
263 def get_merge_message():
264 return gitcmd
.fmt_merge_msg('--file', gitpath('FETCH_HEAD'))
266 def config_dict(local
=True):
270 argv
= ['--global', '--list' ]
271 return config_to_dict(
272 gitcmd
.config(*argv
).splitlines())
274 def config_set(key
=None, value
=None, local
=True):
275 if key
and value
is not None:
276 # git config category.key value
278 if type(value
) is bool:
279 # git uses "true" and "false"
280 strval
= strval
.lower()
282 argv
= [ key
, strval
]
284 argv
= [ '--global', key
, strval
]
285 return gitcmd
.config(*argv
)
287 msg
= "oops in git.config_set(key=%s,value=%s,local=%s"
288 raise Exception(msg
% (key
, value
, local
))
290 def config_to_dict(config_lines
):
291 """parses the lines from git config --list into a dictionary"""
294 for line
in config_lines
:
295 k
, v
= line
.split('=')
296 k
= k
.replace('.','_') # git -> model
297 if v
== 'true' or v
== 'false':
298 v
= bool(eval(v
.title()))
306 def log_helper(all
=False):
307 """Returns a pair of parallel arrays listing the revision sha1's
308 and commit summaries."""
311 regex
= REV_LIST_REGEX
312 output
= gitcmd
.log(pretty
='oneline', all
=all
)
313 for line
in output
.splitlines():
314 match
= regex
.match(line
)
316 revs
.append(match
.group(1))
317 summaries
.append(match
.group(2))
318 return( revs
, summaries
)
320 def parse_ls_tree(rev
):
321 """Returns a list of(mode, type, sha1, path) tuples."""
322 lines
= gitcmd
.ls_tree(rev
, r
=True).splitlines()
324 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
326 match
= regex
.match(line
)
328 mode
= match
.group(1)
329 objtype
= match
.group(2)
330 sha1
= match
.group(3)
331 filename
= match
.group(4)
332 output
.append((mode
, objtype
, sha1
, filename
,) )
335 def push_helper(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
337 branch_arg
= '%s:%s' % ( local_branch
, remote_branch
)
339 branch_arg
= '+%s:%s' % ( local_branch
, remote_branch
)
340 return gitcmd
.push(remote
, branch_arg
, with_status
=True, tags
=tags
)
342 def remote_url(name
):
343 return gitcmd
.config('remote.%s.url' % name
, get
=True)
345 def rev_list_range(start
, end
):
346 range = '%s..%s' % ( start
, end
)
347 raw_revs
= gitcmd
.rev_list(range, pretty
='oneline')
348 return parse_rev_list(raw_revs
)
350 def reset_helper(*args
, **kwargs
):
351 return gitcmd
.reset('--', *args
, **kwargs
)
353 def parse_rev_list(raw_revs
):
355 for line
in raw_revs
.splitlines():
356 match
= REV_LIST_REGEX
.match(line
)
358 rev_id
= match
.group(1)
359 summary
= match
.group(2)
360 revs
.append((rev_id
, summary
,) )
364 """RETURNS: A tuple of staged, unstaged and untracked file lists."""
366 MODIFIED_TAG
= '# Changed but not updated:'
367 UNTRACKED_TAG
= '# Untracked files:'
369 RGX_RENAMED
= re
.compile(
374 RGX_MODIFIED
= re
.compile(
388 current_dest
= staged
390 for status_line
in gitcmd
.status().splitlines():
391 if status_line
== MODIFIED_TAG
:
393 current_dest
= unstaged
396 elif status_line
== UNTRACKED_TAG
:
397 mode
= UNTRACKED_MODE
398 current_dest
= untracked
401 # Staged/unstaged modified/renamed/deleted files
402 if mode
== STAGED_MODE
or mode
== UNSTAGED_MODE
:
403 match
= RGX_MODIFIED
.match(status_line
)
406 filename
= status_line
.replace(tag
, '')
407 current_dest
.append(filename
)
409 match
= RGX_RENAMED
.match(status_line
)
411 oldname
= match
.group(2)
412 newname
= match
.group(3)
413 current_dest
.append(oldname
)
414 current_dest
.append(newname
)
417 elif mode
is UNTRACKED_MODE
:
418 if status_line
.startswith('#\t'):
419 current_dest
.append(status_line
[2:])
421 return( staged
, unstaged
, untracked
)
423 # Must be executed after all functions are defined
424 gitcmd
.setup_commands()