1 '''TODO: "import stgit"'''
7 from cStringIO
import StringIO
9 # A regex for matching the output of git(log|rev-list) --pretty=oneline
10 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
13 return ' '.join([ utils
.shell_quote(arg
) for arg
in argv
])
15 def git(*args
,**kwargs
):
16 gitcmd
= 'git %s' % args
[0]
17 return utils
.run_cmd(gitcmd
, *args
[1:], **kwargs
)
19 def add(to_add
, verbose
=True):
20 '''Invokes 'git add' to index the filenames in to_add.'''
22 return 'No files to add.'
23 return git('add', verbose
=verbose
, *to_add
)
25 def add_or_remove(to_process
):
26 '''Invokes 'git add' to index the filenames in to_process that exist
27 and 'git rm' for those that do not exist.'''
30 return 'No files to add or remove.'
35 for filename
in to_process
:
36 if os
.path
.exists(filename
):
37 to_add
.append(filename
)
41 if len(to_add
) == len(to_process
):
42 # to_process only contained unremoved files --
43 # short-circuit the removal checks
46 # Process files to remote
47 for filename
in to_process
:
48 if not os
.path
.exists(filename
):
49 to_remove
.append(filename
)
50 output
+ '\n\n' + git('rm',*to_remove
)
52 def apply(filename
, indexonly
=True, reverse
=False):
55 kwargs
['reverse'] = True
57 kwargs
['index'] = True
58 kwargs
['cached'] = True
59 argv
= ['apply', filename
]
60 return git(*argv
, **kwargs
)
62 def branch(name
=None, remote
=False, delete
=False):
64 return git('branch', name
, D
=True)
66 branches
= map(lambda x
: x
.lstrip('* '),
67 git('branch', r
=remote
).splitlines())
70 for branch
in branches
:
71 if branch
.endswith('/HEAD'):
73 remotes
.append(branch
)
77 def cat_file(objtype
, sha1
):
78 return git('cat-file', objtype
, sha1
, raw
=True)
80 def cherry_pick(revs
, commit
=False):
81 """Cherry-picks each revision into the current branch.
82 Returns a list of command output strings (1 per cherry pick)"""
84 if not revs
: return []
86 argv
= [ 'cherry-pick' ]
93 new_argv
= argv
+ [rev
]
94 cherries
.append(git(*new_argv
, **kwargs
))
96 return '\n'.join(cherries
)
99 return git('checkout', *args
)
101 def commit(msg
, amend
=False):
102 '''Creates a git commit.'''
104 if not msg
.endswith('\n'):
107 # Sure, this is a potential "security risk," but if someone
108 # is trying to intercept/re-write commit messages on your system,
109 # then you probably have bigger problems to worry about.
110 tmpfile
= utils
.get_tmp_filename()
116 # Create the commit message file
117 file = open(tmpfile
, 'w')
122 output
= git('commit', F
=tmpfile
, amend
=amend
)
125 return ('git commit -F %s --amend %s\n\n%s'
126 % ( tmpfile
, amend
, output
))
128 def create_branch(name
, base
, track
=False):
129 """Creates a branch starting from base. Pass track=True
130 to create a remote tracking branch."""
131 return git('branch', name
, base
, track
=track
)
133 def current_branch():
134 '''Parses 'git branch' to find the current branch.'''
135 branches
= git('branch').splitlines()
136 for branch
in branches
:
137 if branch
.startswith('* '):
138 return branch
.lstrip('* ')
139 return 'Detached HEAD'
141 def diff(commit
=None,filename
=None, color
=False,
142 cached
=True, with_diff_header
=False,
143 suppress_header
=True, reverse
=False):
144 "Invokes git diff on a filepath."
148 argv
.append('%s^..%s' % (commit
, commit
))
152 if type(filename
) is list:
153 argv
.extend(filename
)
155 argv
.append(filename
)
158 'patch-with-raw': True,
159 'unified': defaults
.DIFF_CONTEXT
,
169 diff_lines
= diff
.splitlines()
173 del_tag
= 'deleted file mode '
176 deleted
= cached
and not os
.path
.exists(filename
)
177 for line
in diff_lines
:
178 if not start
and '@@ ' in line
and ' @@' in line
:
180 if start
or(deleted
and del_tag
in line
):
181 output
.write(line
+ '\n')
185 elif not suppress_header
:
186 output
.write(line
+ '\n')
187 result
= output
.getvalue()
190 return('\n'.join(headers
), result
)
195 return git('diff', 'HEAD^',
196 unified
=defaults
.DIFF_CONTEXT
,
201 unified
=defaults
.DIFF_CONTEXT
,
205 def format_patch(revs
):
206 '''writes patches named by revs to the "patches" directory.'''
213 'patch-with-stat': True,
215 for idx
, rev
in enumerate(revs
):
216 real_idx
= idx
+ num_patches
217 kwargs
['start-number'] = real_idx
218 revarg
= '%s^..%s'%(rev
,rev
)
219 output
.append(git('format-patch', revarg
, **kwargs
))
220 num_patches
+= output
[-1].count('\n')
221 return '\n'.join(output
)
223 def config(key
=None, value
=None, local
=False, asdict
=False):
225 argv
= ['config', key
]
230 'global': local
is False,
231 'get': key
and value
is None,
236 return config_to_dict(git('config', **kwargs
).splitlines())
239 return git('config', key
, **kwargs
)
241 elif key
and value
is not None:
242 # git config category.key value
244 if type(value
) is bool:
245 # git uses "true" and "false"
246 strval
= strval
.lower()
247 return git('config', key
, strval
, **kwargs
)
249 msg
= "oops in git.config(key=%s,value=%s,local=%s,asdict=%s"
250 raise Exception(msg
% (key
, value
, local
, asdict
))
253 def config_to_dict(config_lines
):
254 """parses the lines from git config --list into a dictionary"""
257 for line
in config_lines
:
258 k
, v
= line
.split('=')
259 k
= k
.replace('.','_') # git -> model
260 if v
== 'true' or v
== 'false':
261 v
= bool(eval(v
.title()))
269 def log(oneline
=True, all
=False):
270 '''Returns a pair of parallel arrays listing the revision sha1's
271 and commit summaries.'''
274 kwargs
['pretty'] = 'oneline'
277 regex
= REV_LIST_REGEX
278 output
= git('log', all
=all
, **kwargs
)
279 for line
in output
.splitlines():
280 match
= regex
.match(line
)
282 revs
.append(match
.group(1))
283 summaries
.append(match
.group(2))
284 return( revs
, summaries
)
287 """git ls-files as a list"""
288 return git('ls-files').splitlines()
291 """Returns a list of(mode, type, sha1, path) tuples."""
292 lines
= git('ls-tree', rev
, r
=True).splitlines()
294 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
296 match
= regex
.match(line
)
298 mode
= match
.group(1)
299 objtype
= match
.group(2)
300 sha1
= match
.group(3)
301 filename
= match
.group(4)
302 output
.append((mode
, objtype
, sha1
, filename
,) )
305 def push(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
307 branch_arg
= '%s:%s' % ( local_branch
, remote_branch
)
309 branch_arg
= '+%s:%s' % ( local_branch
, remote_branch
)
310 return git('push', remote
, branch_arg
, with_status
=True, tags
=tags
)
314 return 'No base branch specified to rebase.'
315 return git('rebase', newbase
)
318 return git('remote', without_stderr
=True, *args
).splitlines()
320 def remote_url(name
):
321 return config('remote.%s.url' % name
, local
=True)
323 def reset(to_unstage
):
324 '''Use 'git reset' to unstage files from the index.'''
326 return 'No files to reset.'
328 argv
= [ 'reset', '--' ]
329 argv
.extend(to_unstage
)
332 def rev_list_range(start
, end
):
333 argv
= [ 'rev-list', '--pretty=oneline', start
, end
]
334 raw_revs
= git(*argv
).splitlines()
336 for line
in raw_revs
:
337 match
= REV_LIST_REGEX
.match(line
)
339 rev_id
= match
.group(1)
340 summary
= match
.group(2)
341 revs
.append((rev_id
, summary
,) )
345 return git('show',sha1
)
348 '''Returns a relative path to the git project root.'''
349 return git('rev-parse','--show-cdup')
352 '''RETURNS: A tuple of staged, unstaged and untracked files.
353 ( array(staged), array(unstaged), array(untracked) )'''
355 status_lines
= git('status').splitlines()
357 unstaged_header_seen
= False
358 untracked_header_seen
= False
360 modified_header
= '# Changed but not updated:'
361 modified_regex
= re
.compile('(#\tmodified:\s+'
365 renamed_regex
= re
.compile('(#\trenamed:\s+)(.*?)\s->\s(.*)')
367 untracked_header
= '# Untracked files:'
368 untracked_regex
= re
.compile('#\t(.+)')
375 for status_line
in status_lines
:
376 if untracked_header
in status_line
:
377 untracked_header_seen
= True
379 if not untracked_header_seen
:
381 match
= untracked_regex
.match(status_line
)
383 filename
= match
.group(1)
384 untracked
.append(filename
)
386 # Staged, unstaged, and renamed files
387 for status_line
in status_lines
:
388 if modified_header
in status_line
:
389 unstaged_header_seen
= True
391 match
= modified_regex
.match(status_line
)
394 filename
= status_line
.replace(tag
, '')
395 if unstaged_header_seen
:
396 unstaged
.append(filename
)
398 staged
.append(filename
)
401 match
= renamed_regex
.match(status_line
)
403 oldname
= match
.group(2)
404 newname
= match
.group(3)
405 staged
.append(oldname
)
406 staged
.append(newname
)
408 return( staged
, unstaged
, untracked
)
411 return git('tag').splitlines()