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(.*)')
12 return ' '.join([ utils
.shell_quote(arg
) for arg
in argv
])
14 def git(*args
,**kwargs
):
15 return utils
.run_cmd('git', *args
, **kwargs
)
18 '''Invokes 'git add' to index the filenames in to_add.'''
19 if not to_add
: return 'No files to add.'
20 return git('add', *to_add
)
22 def add_or_remove(to_process
):
23 '''Invokes 'git add' to index the filenames in to_process that exist
24 and 'git rm' for those that do not exist.'''
27 return 'No files to add or remove.'
33 for filename
in to_process
:
34 if os
.path
.exists(filename
):
35 to_add
.append(filename
)
39 if len(to_add
) == len(to_process
):
40 # to_process only contained unremoved files --
41 # short-circuit the removal checks
44 # Process files to remote
45 for filename
in to_process
:
46 if not os
.path
.exists(filename
):
47 to_remove
.append(filename
)
50 def apply(filename
, indexonly
=True, reverse
=False):
52 if reverse
: argv
.append('--reverse')
53 if indexonly
: argv
.extend(['--index', '--cached'])
57 def branch(name
=None, remote
=False, delete
=False):
59 return git('branch', '-D', name
)
62 if remote
: argv
.append('-r')
64 branches
= git(*argv
).splitlines()
65 return map(lambda(x
): x
.lstrip('* '), branches
)
67 def cat_file(objtype
, sha1
):
68 return git('cat-file', objtype
, sha1
, raw
=True)
70 def cherry_pick(revs
, commit
=False):
71 '''Cherry-picks each revision into the current branch.'''
73 return 'No revision selected.'
74 argv
= [ 'cherry-pick' ]
75 if not commit
: argv
.append('-n')
79 new_argv
= argv
+ [rev
]
80 cherries
.append(git(*new_argv
))
82 return os
.linesep
.join(cherries
)
85 return git('checkout', rev
)
87 def commit(msg
, amend
=False):
88 '''Creates a git commit.'''
90 if not msg
.endswith(os
.linesep
):
93 # Sure, this is a potential "security risk," but if someone
94 # is trying to intercept/re-write commit messages on your system,
95 # then you probably have bigger problems to worry about.
96 tmpfile
= utils
.get_tmp_filename()
97 argv
= [ 'commit', '-F', tmpfile
]
99 argv
.append('--amend')
101 # Create the commit message file
102 file = open(tmpfile
, 'w')
110 return quote(argv
) + os
.linesep
*2 + output
112 def create_branch(name
, base
, track
=False):
113 '''Creates a branch starting from base. Pass track=True
114 to create a remote tracking branch.'''
116 return git('branch', '--track', name
, base
)
118 return git('branch', name
, base
)
120 def current_branch():
121 '''Parses 'git branch' to find the current branch.'''
122 branches
= git('branch').splitlines()
123 for branch
in branches
:
124 if branch
.startswith('* '):
125 return branch
.lstrip('* ')
126 return 'Detached HEAD'
128 def diff(commit
=None,filename
=None, color
=False,
129 cached
=True, with_diff_header
=False,
131 "Invokes git diff on a filepath."
134 if reverse
: argv
.append('-R')
135 if color
: argv
.append('--color')
136 if cached
: argv
.append('--cached')
138 deleted
= cached
and not os
.path
.exists(filename
)
142 argv
.append(filename
)
145 argv
.append('%s^..%s' % (commit
,commit
))
148 diff_lines
= diff
.splitlines()
152 del_tag
= 'deleted file mode '
155 for line
in diff_lines
:
156 if not start
and '@@ ' in line
and ' @@' in line
:
158 if start
or(deleted
and del_tag
in line
):
159 output
.write(line
+ '\n')
163 result
= output
.getvalue()
167 return(os
.linesep
.join(headers
), result
)
172 '''Returns the latest diffstat.'''
173 return git('diff','--stat','HEAD^')
175 def format_patch(revs
, use_range
):
176 '''Exports patches revs in the 'ugit-patches' subdirectory.
177 If use_range is True, a commit range is passed to git format-patch.'''
179 argv
= ['format-patch','--thread','--patch-with-stat',
183 header
= 'Generated Patches:'
185 new_argv
= argv
+ ['%s^..%s' %( revs
[-1], revs
[0] )]
186 return git(*new_argv
)
190 for idx
, rev
in enumerate(revs
):
191 real_idx
= str(idx
+ num_patches
)
192 new_argv
= argv
+ ['-1', '--start-number', real_idx
, rev
]
193 output
.append(git(*new_argv
))
194 num_patches
+= output
[-1].count(os
.linesep
)
195 return os
.linesep
.join(output
)
197 def config(key
, value
=None):
198 '''Gets or sets git config values. If value is not None, then
199 the config key will be set. Otherwise, the config value of the
200 config key is returned.'''
201 if value
is not None:
202 return git('config', key
, value
)
204 return git('config', '--get', key
)
206 def log(oneline
=True, all
=False):
207 '''Returns a pair of parallel arrays listing the revision sha1's
208 and commit summaries.'''
211 argv
.append('--pretty=oneline')
216 regex
= REV_LIST_REGEX
218 for line
in output
.splitlines():
219 match
= regex
.match(line
)
221 revs
.append(match
.group(1))
222 summaries
.append(match
.group(2))
223 return( revs
, summaries
)
226 return git('ls-files').splitlines()
229 '''Returns a list of(mode, type, sha1, path) tuples.'''
231 lines
= git('ls-tree', '-r', rev
).splitlines()
233 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
235 match
= regex
.match(line
)
237 mode
= match
.group(1)
238 objtype
= match
.group(2)
239 sha1
= match
.group(3)
240 filename
= match
.group(4)
241 output
.append((mode
, objtype
, sha1
, filename
,) )
244 def push(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
247 argv
.append('--tags')
250 if local_branch
== remote_branch
:
251 argv
.append(local_branch
)
253 if not ffwd
and local_branch
:
254 argv
.append('+%s:%s' % ( local_branch
, remote_branch
))
256 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 argv
= ['remote'] + list(args
)
266 return git(*argv
).splitlines()
268 def remote_url(name
):
269 return config('remote.%s.url' % name
)
271 def reset(to_unstage
):
272 '''Use 'git reset' to unstage files from the index.'''
274 return 'No files to reset.'
276 argv
= [ 'reset', '--' ]
277 argv
.extend(to_unstage
)
281 def rev_list_range(start
, end
):
282 argv
= [ 'rev-list', '--pretty=oneline', start
, end
]
283 raw_revs
= git(*argv
).splitlines()
285 for line
in raw_revs
:
286 match
= REV_LIST_REGEX
.match(line
)
288 rev_id
= match
.group(1)
289 summary
= match
.group(2)
290 revs
.append((rev_id
, summary
,) )
294 return git('show',sha1
)
297 '''Returns a relative path to the git project root.'''
298 return git('rev-parse','--show-cdup')
301 '''RETURNS: A tuple of staged, unstaged and untracked files.
302 ( array(staged), array(unstaged), array(untracked) )'''
304 status_lines
= git('status').splitlines()
306 unstaged_header_seen
= False
307 untracked_header_seen
= False
309 modified_header
= '# Changed but not updated:'
310 modified_regex
= re
.compile('(#\tmodified:\W{3}'
311 + '|#\tnew file:\W{3}'
312 + '|#\tdeleted:\W{4})')
314 renamed_regex
= re
.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
316 untracked_header
= '# Untracked files:'
317 untracked_regex
= re
.compile('#\t(.+)')
324 for status_line
in status_lines
:
325 if untracked_header
in status_line
:
326 untracked_header_seen
= True
328 if not untracked_header_seen
:
330 match
= untracked_regex
.match(status_line
)
332 filename
= match
.group(1)
333 untracked
.append(filename
)
335 # Staged, unstaged, and renamed files
336 for status_line
in status_lines
:
337 if modified_header
in status_line
:
338 unstaged_header_seen
= True
340 match
= modified_regex
.match(status_line
)
343 filename
= status_line
.replace(tag
, '')
344 if unstaged_header_seen
:
345 unstaged
.append(filename
)
347 staged
.append(filename
)
350 match
= renamed_regex
.match(status_line
)
352 oldname
= match
.group(2)
353 newname
= match
.group(3)
354 staged
.append(oldname
)
355 staged
.append(newname
)
357 return( staged
, unstaged
, untracked
)
360 return git('tag').splitlines()