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(to_export
, revs
, output
='patches'):
389 """writes patches named by to_export to the output directory."""
393 cur_rev
= to_export
[0]
394 cur_master_idx
= revs
.index(cur_rev
)
396 patches_to_export
= [ [cur_rev
] ]
399 for idx
, rev
in enumerate(to_export
[1:]):
400 # Limit the search to the current neighborhood for efficiency
401 master_idx
= revs
[ cur_master_idx
: ].index(rev
)
402 master_idx
+= cur_master_idx
403 if master_idx
== cur_master_idx
+ 1:
404 patches_to_export
[ patchset_idx
].append(rev
)
408 patches_to_export
.append([ rev
])
409 cur_master_idx
= master_idx
412 for patchset
in patches_to_export
:
413 revarg
= '%s^..%s' % (patchset
[0], patchset
[-1])
424 return '\n'.join(outlines
)
426 def get_merge_message():
427 return gitcmd
.fmt_merge_msg('--file', git_repo_path('FETCH_HEAD'))
429 def config_set(key
=None, value
=None, local
=True):
430 if key
and value
is not None:
431 # git config category.key value
433 if type(value
) is bool:
434 # git uses "true" and "false"
435 strval
= strval
.lower()
437 argv
= [ key
, strval
]
439 argv
= [ '--global', key
, strval
]
440 return gitcmd
.config(*argv
)
442 msg
= "oops in git.config_set(key=%s,value=%s,local=%s"
443 raise Exception(msg
% (key
, value
, local
))
445 def config_dict(local
=True):
449 argv
= ['--global', '--list' ]
450 return config_to_dict(
451 gitcmd
.config(*argv
).splitlines())
453 def config_to_dict(config_lines
):
454 """parses the lines from git config --list into a dictionary"""
457 for line
in config_lines
:
458 k
, v
= line
.split('=', 1)
459 k
= k
.replace('.','_') # git -> model
460 if v
== 'true' or v
== 'false':
461 v
= bool(eval(v
.title()))
469 def log_helper(all
=False):
470 """Returns a pair of parallel arrays listing the revision sha1's
471 and commit summaries."""
474 regex
= REV_LIST_REGEX
475 output
= gitcmd
.log(pretty
='oneline', all
=all
)
476 for line
in output
.splitlines():
477 match
= regex
.match(line
)
479 revs
.append(match
.group(1))
480 summaries
.append(match
.group(2))
481 return( revs
, summaries
)
483 def parse_ls_tree(rev
):
484 """Returns a list of(mode, type, sha1, path) tuples."""
485 lines
= gitcmd
.ls_tree(rev
, r
=True).splitlines()
487 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
489 match
= regex
.match(line
)
491 mode
= match
.group(1)
492 objtype
= match
.group(2)
493 sha1
= match
.group(3)
494 filename
= match
.group(4)
495 output
.append((mode
, objtype
, sha1
, filename
,) )
498 def push_helper(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
500 branch_arg
= '%s:%s' % ( local_branch
, remote_branch
)
502 branch_arg
= '+%s:%s' % ( local_branch
, remote_branch
)
503 return gitcmd
.push(remote
, branch_arg
, with_status
=True, tags
=tags
)
505 def remote_url(name
):
506 return gitcmd
.config('remote.%s.url' % name
, get
=True)
508 def rev_list_range(start
, end
):
509 range = '%s..%s' % ( start
, end
)
510 raw_revs
= gitcmd
.rev_list(range, pretty
='oneline')
511 return parse_rev_list(raw_revs
)
513 def git_repo_path(*subpaths
):
514 paths
= [ gitcmd
.rev_parse(git_dir
=True) ]
515 paths
.extend(subpaths
)
516 return os
.path
.realpath(os
.path
.join(*paths
))
518 def get_merge_message_path():
519 for file in ('MERGE_MSG', 'SQUASH_MSG'):
520 path
= git_repo_path(file)
521 if os
.path
.exists(path
):
525 def reset_helper(*args
, **kwargs
):
526 return gitcmd
.reset('--', *args
, **kwargs
)
528 def parse_rev_list(raw_revs
):
530 for line
in raw_revs
.splitlines():
531 match
= REV_LIST_REGEX
.match(line
)
533 rev_id
= match
.group(1)
534 summary
= match
.group(2)
535 revs
.append((rev_id
, summary
,) )
539 """RETURNS: A tuple of staged, unstaged and untracked file lists.
542 """handles quoted paths."""
543 if path
.startswith('"') and path
.endswith('"'):
548 MODIFIED_TAG
= '# Changed but not updated:'
549 UNTRACKED_TAG
= '# Untracked files:'
550 RGX_RENAMED
= re
.compile(
554 RGX_MODIFIED
= re
.compile(
567 current_dest
= staged
570 for status_line
in gitcmd
.status().splitlines():
571 if status_line
== MODIFIED_TAG
:
573 current_dest
= unstaged
575 elif status_line
== UNTRACKED_TAG
:
576 mode
= UNTRACKED_MODE
577 current_dest
= untracked
579 # Staged/unstaged modified/renamed/deleted files
580 if mode
is STAGED_MODE
or mode
is UNSTAGED_MODE
:
581 match
= RGX_MODIFIED
.match(status_line
)
584 filename
= status_line
.replace(tag
, '')
585 current_dest
.append(eval_path(filename
))
587 match
= RGX_RENAMED
.match(status_line
)
589 oldname
= match
.group(2)
590 newname
= match
.group(3)
591 current_dest
.append(eval_path(oldname
))
592 current_dest
.append(eval_path(newname
))
595 elif mode
is UNTRACKED_MODE
:
596 if status_line
.startswith('#\t'):
597 current_dest
.append(eval_path(status_line
[2:]))
599 return( staged
, unstaged
, untracked
)
601 # Must be executed after all functions are defined
602 gitcmd
.setup_commands()