7 from cStringIO
import StringIO
10 def set_diff_context(ctxt
):
14 def get_tmp_filename():
15 # Allow TMPDIR/TMP with a fallback to /tmp
17 return os
.path
.join(env
.get('TMP', env
.get('TMPDIR', '/tmp')),
18 '.git.%s.%s' % ( os
.getpid(), time
.time()))
20 def run_cmd(cmd
, *args
, **kwargs
):
22 Returns an array of strings from the command's output.
27 Passing raw=True prevents the output from being striped.
31 Passing with_status=True returns tuple(status,output)
32 instead of just the command's output.
34 run_command("git foo", bar, buzz,
35 baz=value, bar=True, q=True, f='foo')
40 "--bar", "--baz=value",
49 raw
= pop_key(kwargs
, 'raw')
50 with_status
= pop_key(kwargs
,'with_status')
51 with_stderr
= not pop_key(kwargs
,'without_stderr')
55 for k
,v
in kwargs
.iteritems():
57 k
= k
.replace('_','-')
59 kwarglist
.append("--%s" % k
)
60 elif v
is not None and type(v
) is not bool:
61 kwarglist
.append("--%s=%s" % (k
,v
))
64 kwarglist
.append("-%s" % k
)
65 elif v
is not None and type(v
) is not bool:
66 kwarglist
.append("-%s" % k
)
67 kwarglist
.append(str(v
))
68 # Handle cmd as either a string or an argv list
70 # we only call run_cmd(str) with str='git command'
71 # or other simple commands
76 cmd
= tuple(cmd
+ kwarglist
+ list(args
))
80 stderr
= subprocess
.STDOUT
82 proc
= subprocess
.Popen(cmd
, cwd
=cwd
,
83 stdout
=subprocess
.PIPE
,
86 # Wait for the process to return
87 output
, err
= proc
.communicate()
88 # conveniently strip off trailing newlines
90 output
= output
.rstrip()
92 raise RuntimeError("%s return exit status %d"
99 # union of functions in this file and dynamic functions
100 # defined in the git command string list below
101 def git(*args
,**kwargs
):
102 """This is a convenience wrapper around run_cmd that
103 sets things up so that commands are run in the canonical
104 'git command [options] [args]' form."""
105 cmd
= 'git %s' % args
[0]
106 return run_cmd(cmd
, *args
[1:], **kwargs
)
108 class GitCommand(object):
109 """This class wraps this module so that arbitrary git commands
110 can be dynamically called at runtime."""
111 def __init__(self
, module
):
114 # This creates git.foo() methods dynamically for each of the
115 # following names at import-time.
186 """.split(): getattr(self
, cmd
)
188 def setup_commands(self
):
189 # Import the functions from the module
190 for name
, val
in self
.module
.__dict
__.iteritems():
191 if type(val
) is types
.FunctionType
:
192 setattr(self
, name
, val
)
193 # Import dynamic functions and those from the module
194 # functions into self.commands
195 for name
, val
in self
.__dict
__.iteritems():
196 if type(val
) is types
.FunctionType
:
197 self
.commands
[name
] = val
199 def __getattr__(self
, cmd
):
200 if hasattr(self
.module
, cmd
):
201 value
= getattr(self
.module
, cmd
)
202 setattr(self
, cmd
, value
)
204 def git_cmd(*args
, **kwargs
):
205 """Runs "git <cmd> [options] [args]"
206 The output is returned as a string.
207 Pass with_stauts=True to merge stderr's into stdout.
208 Pass raw=True to avoid stripping git's output.
209 Finally, pass with_status=True to
210 return a (status, output) tuple."""
211 return git(cmd
.replace('_','-'), *args
, **kwargs
)
212 setattr(self
, cmd
, git_cmd
)
215 # core git wrapper for use in this module
216 gitcmd
= GitCommand(sys
.modules
[__name__
])
217 sys
.modules
[__name__
] = gitcmd
219 #+-------------------------------------------------------------------------
220 #+ A regex for matching the output of git(log|rev-list) --pretty=oneline
221 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
225 output
= gitcmd
.read_tree("HEAD", reset
=True, u
=True, v
=True)
227 merge_head
= git_repo_path('MERGE_HEAD')
228 if os
.path
.exists(merge_head
):
229 os
.unlink(merge_head
)
230 # remove MERGE_MESSAGE, etc.
231 merge_msg_path
= get_merge_message_path()
232 while merge_msg_path
is not None:
233 os
.unlink(merge_msg_path
)
234 merge_msg_path
= get_merge_message_path()
236 def add_or_remove(*to_process
):
237 """Invokes 'git add' to index the filenames in to_process that exist
238 and 'git rm' for those that do not exist."""
241 return 'No files to add or remove.'
246 for filename
in to_process
:
247 if os
.path
.exists(filename
):
248 to_add
.append(filename
)
250 output
= gitcmd
.add(verbose
=True, *to_add
)
252 if len(to_add
) == len(to_process
):
253 # to_process only contained unremoved files --
254 # short-circuit the removal checks
257 # Process files to remote
258 for filename
in to_process
:
259 if not os
.path
.exists(filename
):
260 to_remove
.append(filename
)
261 output
+ '\n\n' + gitcmd
.rm(*to_remove
)
263 def branch_list(remote
=False):
264 branches
= map(lambda x
: x
.lstrip('* '),
265 gitcmd
.branch(r
=remote
).splitlines())
268 for branch
in branches
:
269 if branch
.endswith('/HEAD'):
271 remotes
.append(branch
)
275 def cherry_pick_list(revs
, **kwargs
):
276 """Cherry-picks each revision into the current branch.
277 Returns a list of command output strings (1 per cherry pick)"""
282 cherries
.append(gitcmd
.cherry_pick(rev
, **kwargs
))
283 return '\n'.join(cherries
)
285 def commit_with_msg(msg
, amend
=False):
286 """Creates a git commit."""
288 if not msg
.endswith('\n'):
290 # Sure, this is a potential "security risk," but if someone
291 # is trying to intercept/re-write commit messages on your system,
292 # then you probably have bigger problems to worry about.
293 tmpfile
= get_tmp_filename()
298 # Create the commit message file
299 file = open(tmpfile
, 'w')
304 output
= gitcmd
.commit(F
=tmpfile
, amend
=amend
)
307 return ('git commit -F %s --amend %s\n\n%s'
308 % ( tmpfile
, amend
, output
))
310 def create_branch(name
, base
, track
=False):
311 """Creates a branch starting from base. Pass track=True
312 to create a remote tracking branch."""
313 return gitcmd
.branch(name
, base
, track
=track
)
315 def current_branch():
316 """Parses 'git branch' to find the current branch."""
318 branches
= gitcmd
.branch().splitlines()
319 for branch
in branches
:
320 if branch
.startswith('* '):
321 return branch
.lstrip('* ')
322 return 'Detached HEAD'
324 def diff_helper(commit
=None,
328 with_diff_header
=False,
329 suppress_header
=True,
331 "Invokes git diff on a filepath."
335 argv
.append('%s^..%s' % (commit
, commit
))
339 if type(filename
) is list:
340 argv
.extend(filename
)
342 argv
.append(filename
)
349 unified
=DIFF_CONTEXT
,
355 del_tag
= 'deleted file mode '
358 deleted
= cached
and not os
.path
.exists(filename
)
360 if not start
and '@@ ' in line
and ' @@' in line
:
362 if start
or(deleted
and del_tag
in line
):
363 output
.write(line
+ '\n')
367 elif not suppress_header
:
368 output
.write(line
+ '\n')
369 result
= output
.getvalue()
372 return('\n'.join(headers
), result
)
379 unified
=DIFF_CONTEXT
,
384 unified
=DIFF_CONTEXT
,
388 def format_patch_helper(output
='patches', *revs
):
389 """writes patches named by revs to the output directory."""
392 for idx
, rev
in enumerate(revs
):
393 real_idx
= idx
+ num_patches
394 revarg
= '%s^..%s' % (rev
,rev
)
399 start_number
=real_idx
,
405 num_patches
+= output
[-1].count('\n')
406 return '\n'.join(output
)
408 def get_merge_message():
409 return gitcmd
.fmt_merge_msg('--file', git_repo_path('FETCH_HEAD'))
411 def config_set(key
=None, value
=None, local
=True):
412 if key
and value
is not None:
413 # git config category.key value
415 if type(value
) is bool:
416 # git uses "true" and "false"
417 strval
= strval
.lower()
419 argv
= [ key
, strval
]
421 argv
= [ '--global', key
, strval
]
422 return gitcmd
.config(*argv
)
424 msg
= "oops in git.config_set(key=%s,value=%s,local=%s"
425 raise Exception(msg
% (key
, value
, local
))
427 def config_dict(local
=True):
431 argv
= ['--global', '--list' ]
432 return config_to_dict(
433 gitcmd
.config(*argv
).splitlines())
435 def config_to_dict(config_lines
):
436 """parses the lines from git config --list into a dictionary"""
439 for line
in config_lines
:
440 k
, v
= line
.split('=', 1)
441 k
= k
.replace('.','_') # git -> model
442 if v
== 'true' or v
== 'false':
443 v
= bool(eval(v
.title()))
451 def log_helper(all
=False):
452 """Returns a pair of parallel arrays listing the revision sha1's
453 and commit summaries."""
456 regex
= REV_LIST_REGEX
457 output
= gitcmd
.log(pretty
='oneline', all
=all
)
458 for line
in output
.splitlines():
459 match
= regex
.match(line
)
461 revs
.append(match
.group(1))
462 summaries
.append(match
.group(2))
463 return( revs
, summaries
)
465 def parse_ls_tree(rev
):
466 """Returns a list of(mode, type, sha1, path) tuples."""
467 lines
= gitcmd
.ls_tree(rev
, r
=True).splitlines()
469 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
471 match
= regex
.match(line
)
473 mode
= match
.group(1)
474 objtype
= match
.group(2)
475 sha1
= match
.group(3)
476 filename
= match
.group(4)
477 output
.append((mode
, objtype
, sha1
, filename
,) )
480 def push_helper(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
482 branch_arg
= '%s:%s' % ( local_branch
, remote_branch
)
484 branch_arg
= '+%s:%s' % ( local_branch
, remote_branch
)
485 return gitcmd
.push(remote
, branch_arg
, with_status
=True, tags
=tags
)
487 def remote_url(name
):
488 return gitcmd
.config('remote.%s.url' % name
, get
=True)
490 def rev_list_range(start
, end
):
491 range = '%s..%s' % ( start
, end
)
492 raw_revs
= gitcmd
.rev_list(range, pretty
='oneline')
493 return parse_rev_list(raw_revs
)
495 def git_repo_path(*subpaths
):
496 paths
= [ gitcmd
.rev_parse(git_dir
=True) ]
497 paths
.extend(subpaths
)
498 return os
.path
.realpath(os
.path
.join(*paths
))
500 def get_merge_message_path():
501 for file in ('MERGE_MSG', 'SQUASH_MSG'):
502 path
= git_repo_path(file)
503 if os
.path
.exists(path
):
507 def reset_helper(*args
, **kwargs
):
508 return gitcmd
.reset('--', *args
, **kwargs
)
510 def parse_rev_list(raw_revs
):
512 for line
in raw_revs
.splitlines():
513 match
= REV_LIST_REGEX
.match(line
)
515 rev_id
= match
.group(1)
516 summary
= match
.group(2)
517 revs
.append((rev_id
, summary
,) )
521 """RETURNS: A tuple of staged, unstaged and untracked file lists.
524 """handles quoted paths."""
525 if path
.startswith('"') and path
.endswith('"'):
530 MODIFIED_TAG
= '# Changed but not updated:'
531 UNTRACKED_TAG
= '# Untracked files:'
532 RGX_RENAMED
= re
.compile(
536 RGX_MODIFIED
= re
.compile(
549 current_dest
= staged
552 for status_line
in gitcmd
.status().splitlines():
553 if status_line
== MODIFIED_TAG
:
555 current_dest
= unstaged
557 elif status_line
== UNTRACKED_TAG
:
558 mode
= UNTRACKED_MODE
559 current_dest
= untracked
561 # Staged/unstaged modified/renamed/deleted files
562 if mode
is STAGED_MODE
or mode
is UNSTAGED_MODE
:
563 match
= RGX_MODIFIED
.match(status_line
)
566 filename
= status_line
.replace(tag
, '')
567 current_dest
.append(eval_path(filename
))
569 match
= RGX_RENAMED
.match(status_line
)
571 oldname
= match
.group(2)
572 newname
= match
.group(3)
573 current_dest
.append(eval_path(oldname
))
574 current_dest
.append(eval_path(newname
))
577 elif mode
is UNTRACKED_MODE
:
578 if status_line
.startswith('#\t'):
579 current_dest
.append(eval_path(status_line
[2:]))
581 return( staged
, unstaged
, untracked
)
583 # Must be executed after all functions are defined
584 gitcmd
.setup_commands()