1 '''TODO: "import stgit"'''
6 from cStringIO
import StringIO
8 # A regex for matching the output of git(log|rev-list) --pretty=oneline
9 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 return utils
.run_cmd('git', *args
, **kwargs
)
19 '''Invokes 'git add' to index the filenames in to_add.'''
20 if not to_add
: return 'No files to add.'
21 return git('add', *to_add
)
23 def add_or_remove(to_process
):
24 '''Invokes 'git add' to index the filenames in to_process that exist
25 and 'git rm' for those that do not exist.'''
28 return 'No files to add or remove.'
34 for filename
in to_process
:
35 if os
.path
.exists(filename
):
36 to_add
.append(filename
)
40 if len(to_add
) == len(to_process
):
41 # to_process only contained unremoved files --
42 # short-circuit the removal checks
45 # Process files to remote
46 for filename
in to_process
:
47 if not os
.path
.exists(filename
):
48 to_remove
.append(filename
)
51 def apply(filename
, indexonly
=True, reverse
=False):
53 if reverse
: argv
.append('--reverse')
54 if indexonly
: argv
.extend(['--index', '--cached'])
58 def branch(name
=None, remote
=False, delete
=False):
60 return git('branch', '-D', name
)
63 if remote
: argv
.append('-r')
65 branches
= git(*argv
).splitlines()
66 return map(lambda(x
): x
.lstrip('* '), branches
)
68 def cat_file(objtype
, sha1
):
69 return git('cat-file', objtype
, sha1
, raw
=True)
71 def cherry_pick(revs
, commit
=False):
72 '''Cherry-picks each revision into the current branch.'''
74 return 'No revision selected.'
75 argv
= [ 'cherry-pick' ]
76 if not commit
: argv
.append('-n')
80 new_argv
= argv
+ [rev
]
81 cherries
.append(git(*new_argv
))
83 return os
.linesep
.join(cherries
)
86 return git('checkout', rev
)
88 def commit(msg
, amend
=False):
89 '''Creates a git commit.'''
91 if not msg
.endswith(os
.linesep
):
94 # Sure, this is a potential "security risk," but if someone
95 # is trying to intercept/re-write commit messages on your system,
96 # then you probably have bigger problems to worry about.
97 tmpfile
= utils
.get_tmp_filename()
98 argv
= [ 'commit', '-F', tmpfile
]
100 argv
.append('--amend')
102 # Create the commit message file
103 file = open(tmpfile
, 'w')
111 return quote(argv
) + os
.linesep
*2 + output
113 def create_branch(name
, base
, track
=False):
114 '''Creates a branch starting from base. Pass track=True
115 to create a remote tracking branch.'''
117 return git('branch', '--track', name
, base
)
119 return git('branch', name
, base
)
121 def current_branch():
122 '''Parses 'git branch' to find the current branch.'''
123 branches
= git('branch').splitlines()
124 for branch
in branches
:
125 if branch
.startswith('* '):
126 return branch
.lstrip('* ')
127 return 'Detached HEAD'
129 def diff(commit
=None,filename
=None, color
=False,
130 cached
=True, with_diff_header
=False,
131 suppress_header
=True, reverse
=False):
132 "Invokes git diff on a filepath."
134 argv
= [ 'diff', '--unified='+str(DIFF_CONTEXT
), '--patch-with-raw']
135 if reverse
: argv
.append('-R')
136 if color
: argv
.append('--color')
137 if cached
: argv
.append('--cached')
139 deleted
= cached
and not os
.path
.exists(filename
)
143 argv
.append(filename
)
146 argv
.append('%s^..%s' % (commit
,commit
))
149 diff_lines
= diff
.splitlines()
153 del_tag
= 'deleted file mode '
156 for line
in diff_lines
:
157 if not start
and '@@ ' in line
and ' @@' in line
:
159 if start
or(deleted
and del_tag
in line
):
161 output
.write(os
.linesep
)
165 elif not suppress_header
:
167 output
.write(os
.linesep
)
169 result
= output
.getvalue()
173 return(os
.linesep
.join(headers
), result
)
178 return git('diff','--stat','HEAD^')
180 def format_patch(revs
):
181 '''writes patches named by revs to the "patches" directory.'''
184 argv
= ['format-patch','--thread','--patch-with-stat', '-o','patches']
187 for idx
, rev
in enumerate(revs
):
188 real_idx
= str(idx
+ num_patches
)
189 new_argv
= argv
+ ['--start-number', real_idx
,
191 output
.append(git(*new_argv
))
192 num_patches
+= output
[-1].count(os
.linesep
)
193 return os
.linesep
.join(output
)
195 def config(key
, value
=None, local
=True):
198 argv
.append('--global')
204 if type(value
) is bool:
205 value
= str(value
).lower()
206 argv
.append(str(value
))
209 def log(oneline
=True, all
=False):
210 '''Returns a pair of parallel arrays listing the revision sha1's
211 and commit summaries.'''
214 argv
.append('--pretty=oneline')
219 regex
= REV_LIST_REGEX
221 for line
in output
.splitlines():
222 match
= regex
.match(line
)
224 revs
.append(match
.group(1))
225 summaries
.append(match
.group(2))
226 return( revs
, summaries
)
229 return git('ls-files').splitlines()
232 '''Returns a list of(mode, type, sha1, path) tuples.'''
233 lines
= git('ls-tree', '-r', rev
).splitlines()
235 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
237 match
= regex
.match(line
)
239 mode
= match
.group(1)
240 objtype
= match
.group(2)
241 sha1
= match
.group(3)
242 filename
= match
.group(4)
243 output
.append((mode
, objtype
, sha1
, filename
,) )
246 def push(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
249 argv
.append('--tags')
251 if local_branch
== remote_branch
:
252 argv
.append(local_branch
)
254 if not ffwd
and local_branch
:
255 argv
.append('+%s:%s' % ( local_branch
, remote_branch
))
257 argv
.append('%s:%s' % ( local_branch
, remote_branch
))
258 return git(with_status
=True, *argv
)
261 if not newbase
: return
262 return git('rebase', newbase
)
265 return git('remote', stderr
=False, *args
).splitlines()
267 def remote_url(name
):
268 return config('remote.%s.url' % name
)
270 def reset(to_unstage
):
271 '''Use 'git reset' to unstage files from the index.'''
273 return 'No files to reset.'
275 argv
= [ 'reset', '--' ]
276 argv
.extend(to_unstage
)
280 def rev_list_range(start
, end
):
281 argv
= [ 'rev-list', '--pretty=oneline', start
, end
]
282 raw_revs
= git(*argv
).splitlines()
284 for line
in raw_revs
:
285 match
= REV_LIST_REGEX
.match(line
)
287 rev_id
= match
.group(1)
288 summary
= match
.group(2)
289 revs
.append((rev_id
, summary
,) )
293 return git('show',sha1
)
296 '''Returns a relative path to the git project root.'''
297 return git('rev-parse','--show-cdup')
300 '''RETURNS: A tuple of staged, unstaged and untracked files.
301 ( array(staged), array(unstaged), array(untracked) )'''
303 status_lines
= git('status').splitlines()
305 unstaged_header_seen
= False
306 untracked_header_seen
= False
308 modified_header
= '# Changed but not updated:'
309 modified_regex
= re
.compile('(#\tmodified:\W{3}'
310 + '|#\tnew file:\W{3}'
311 + '|#\tdeleted:\W{4})')
313 renamed_regex
= re
.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
315 untracked_header
= '# Untracked files:'
316 untracked_regex
= re
.compile('#\t(.+)')
323 for status_line
in status_lines
:
324 if untracked_header
in status_line
:
325 untracked_header_seen
= True
327 if not untracked_header_seen
:
329 match
= untracked_regex
.match(status_line
)
331 filename
= match
.group(1)
332 untracked
.append(filename
)
334 # Staged, unstaged, and renamed files
335 for status_line
in status_lines
:
336 if modified_header
in status_line
:
337 unstaged_header_seen
= True
339 match
= modified_regex
.match(status_line
)
342 filename
= status_line
.replace(tag
, '')
343 if unstaged_header_seen
:
344 unstaged
.append(filename
)
346 staged
.append(filename
)
349 match
= renamed_regex
.match(status_line
)
351 oldname
= match
.group(2)
352 newname
= match
.group(3)
353 staged
.append(oldname
)
354 staged
.append(newname
)
356 return( staged
, unstaged
, untracked
)
359 return git('tag').splitlines()