7 # A regex for matching the output of git (log|rev-list) --pretty=oneline
8 REV_LIST_PATTERN
= '([0-9a-f]+)\W(.*)'
11 '''Invokes 'git add' to index the filenames in to_add.'''
13 if not to_add
: return 'ERROR: No files to add.'
15 argv
= [ 'git', 'add' ]
16 for filename
in to_add
:
17 argv
.append (utils
.shell_quote (filename
))
20 return 'Running:\t%s\n%s\n%s added successfully' % (
21 cmd
, commands
.getoutput (cmd
), ', '.join (to_add
) )
23 def git_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.'''
27 if not to_process
: return 'ERROR: No files to add or remove.'
32 for filename
in to_process
:
33 if os
.path
.exists (filename
):
34 to_add
.append (filename
)
37 output
+= git_add (to_add
) + '\n\n'
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 add
45 argv
= [ 'git', 'rm' ]
46 for filename
in to_process
:
47 if not os
.path
.exists (filename
):
48 argv
.append (utils
.shell_quote (filename
))
51 return output
+ 'Running: %s\n%s' % ( cmd
, commands
.getoutput (cmd
) )
53 def git_branch (remote
=False):
54 '''Returns a list of git branches.
55 Pass "remote=True" to list remote branches.'''
57 if remote
: cmd
+= ' -r'
58 branches
= commands
.getoutput (cmd
).split ('\n')
59 return map (lambda (x
): x
.lstrip ('* '), branches
)
61 def git_cat_file (objtype
, sha1
, target_file
=None):
62 cmd
= 'git cat-file %s %s' % ( objtype
, sha1
)
64 cmd
+= '> %s' % utils
.shell_quote (target_file
)
65 return commands
.getoutput (cmd
)
67 def git_cherry_pick (revs
, commit
=False):
68 '''Cherry-picks each revision into the current branch.'''
70 return 'ERROR: No revisions selected for cherry-picking.'''
72 cmd = 'git cherry
-pick
'
73 if not commit: cmd += '-n
'
76 output.append ('Cherry
-picking
: ' + rev)
77 output.append (commands.getoutput (cmd + rev))
79 return '\n'.join (output)
81 def git_commit (msg, amend, files):
82 '''Creates a git commit. 'commit_all
' triggers the -a
83 flag to 'git commit
.' 'amend
' triggers --amend.
84 'files
' is a list of files to use for commits without -a.'''
86 # Allow TMPDIR/TMP with a fallback to /tmp
87 tmpdir = os.getenv ('TMPDIR
', os.getenv ('TMP
', '/tmp
'))
89 # Sure, this is a potential "security risk," but if someone
90 # is trying to intercept/re-write commit messages on your system,
91 # then you probably have bigger problems to worry about.
92 tmpfile = os.path.join (tmpdir,
93 'ugit
.%s.%s' % ( os.getuid(), time.time() ))
95 argv = [ 'git
', 'commit
', '-F
', tmpfile ]
97 if amend: argv.append ('--amend
')
100 return 'ERROR
: No files selected
for commit
.'
104 argv.append (utils.shell_quote (file))
106 # Create the commit message file
107 file = open (tmpfile, 'w
')
112 cmd = ' '.join (argv)
113 output = commands.getoutput (cmd)
116 return 'Running
:\t%s\n%s' % (cmd, output)
118 def git_create_branch (name, base, track=False):
119 '''Creates a branch starting from base. Pass track=True
120 to create a remote tracking branch.'''
122 if track: cmd += ' --track
'
123 cmd += '%s %s' % ( utils.shell_quote (name),
124 utils.shell_quote (base))
125 return commands.getoutput (cmd)
128 def git_current_branch():
129 '''Parses 'git branch
' to find the current branch.'''
130 branches = commands.getoutput ('git branch
').split ('\n')
131 for branch in branches:
132 if branch.startswith ('* '):
133 return branch.lstrip ('* ')
134 raise Exception, 'No current branch
. Detached HEAD?
'
136 def git_diff (filename, staged=True):
137 '''Invokes git_diff on filename. Passing staged=True adds
138 diffs the index against HEAD (i.e. --cached).'''
141 argv = [ 'git
', 'diff
', '--color
']
143 deleted = not os.path.exists (filename)
144 argv.append ('--cached
')
147 argv.append (utils.shell_quote (filename))
149 diff = commands.getoutput (' '.join (argv))
150 diff_lines = diff.split ('\n')
154 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 return '\n'.join (output)
163 def git_diff_stat ():
164 '''Returns the latest diffstat.'''
165 return commands.getoutput ('git diff
--color
--stat HEAD^
')
167 def git_format_patch (revs, use_range):
168 '''Exports patches revs in the 'ugit
-patches
' subdirectory.
169 If use_range is True, a commit range is passed to git format-patch.'''
171 cmd = 'git format
-patch
--thread
--patch
-with
-stat
-o ugit
-patches
'
172 header = 'Generated Patches
:'
177 rev_range = '%s^
..%s' % ( revs[-1], revs[0] )
178 return header + '\n' + commands.getoutput (cmd + rev_range)
182 for idx, rev in enumerate (revs):
183 real_idx = idx + num_patches
184 revcmd = cmd + '-1 --start
-number
%d %s' % (real_idx, rev)
185 output.append (commands.getoutput (revcmd))
186 num_patches += output[-1].count ('\n')
187 return '\n'.join (output)
189 def git_config(key, value=None):
190 '''Gets or sets git config values. If value is not None, then
191 the config key will be set. Otherwise, the config value of the
192 config key is returned.'''
193 k = utils.shell_quote (key)
194 if value is not None:
195 v = utils.shell_quote (value)
196 return commands.getoutput ('git config
--set %s %s' % (k, v))
198 return commands.getoutput ('git config
--get
%s' % k)
200 def git_log (oneline=True, all=False):
201 '''Returns a pair of parallel arrays listing the revision sha1's
202 and commit summaries
.'''
203 argv = [ 'git', 'log' ]
204 if oneline: argv.append ('--pretty=oneline')
205 if all: argv.append ('--all')
208 regex = re.compile (REV_LIST_PATTERN)
209 output = commands.getoutput (' '.join (argv))
210 for line in output.split ('\n'):
211 match = regex.match (line)
213 revs.append (match.group (1))
214 summaries.append (match.group (2))
215 return ( revs, summaries )
217 def git_ls_tree (rev):
218 '''Returns a
list of (mode
, type, sha1
, path
) tuples
.'''
219 regex = re.compile ('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
220 sh_rev = utils.shell_quote (rev)
221 lines = commands.getoutput ('git ls-tree -r ' + sh_rev).split ('\n')
224 match = regex.match (line)
226 mode = match.group (1)
227 objtype = match.group (2)
228 sha1 = match.group (3)
229 filename = match.group (4)
230 output.append ( (mode, objtype, sha1, filename,) )
233 def git_reset (to_unstage):
234 '''Use
'git reset' to unstage files
from the index
.'''
236 if not to_unstage: return 'ERROR: No files to reset.'
238 argv = [ 'git', 'reset', '--' ]
239 for filename in to_unstage:
240 argv.append (utils.shell_quote (filename))
242 cmd = ' '.join (argv)
243 return 'Running:\t%s\n%s' % ( cmd, commands.getoutput (cmd) )
245 def git_rev_list_range (rev_start, rev_end):
246 cmd = ('git rev-list --pretty=oneline %s..%s'
247 % (utils.shell_quote (rev_start), utils.shell_quote(rev_end)))
250 raw_revs = commands.getoutput (cmd).split ('\n')
251 regex = re.compile (REV_LIST_PATTERN)
252 for line in raw_revs:
253 match = regex.match (line)
255 rev_id = match.group (1)
256 summary = match.group (2)
257 revs.append ( (rev_id, summary,) )
261 def git_show (sha1, color=False):
263 if color: cmd += '--color '
264 return commands.getoutput (cmd + sha1)
267 '''Returns a relative path to the git project root
.'''
268 return commands.getoutput ('git rev-parse --show-cdup')
271 '''RETURNS
: A
tuple of staged
, unstaged
and untracked files
.
272 ( array(staged
), array(unstaged
), array(untracked
) )'''
274 status_lines = commands.getoutput ('git status').split ('\n')
276 unstaged_header_seen = False
277 untracked_header_seen = False
279 modified_header = '# Changed but not updated:'
280 modified_regex = re.compile ('(#\tmodified:\W{3}'
281 + '|#\tnew file:\W{3}'
282 + '|#\tdeleted:\W{4})')
284 renamed_regex = re.compile ('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
286 untracked_header = '# Untracked files:'
287 untracked_regex = re.compile ('#\t(.+)')
294 for status_line in status_lines:
295 if untracked_header in status_line:
296 untracked_header_seen = True
298 if not untracked_header_seen:
300 match = untracked_regex.match (status_line)
302 filename = match.group (1)
303 untracked.append (filename)
305 # Staged, unstaged, and renamed files
306 for status_line in status_lines:
307 if modified_header in status_line:
308 unstaged_header_seen = True
310 match = modified_regex.match (status_line)
312 tag = match.group (0)
313 filename = status_line.replace (tag, '')
314 if unstaged_header_seen:
315 unstaged.append (filename)
317 staged.append (filename)
320 match = renamed_regex.match (status_line)
322 oldname = match.group (2)
323 newname = match.group (3)
324 staged.append (oldname)
325 staged.append (newname)
327 return ( staged, unstaged, untracked )
330 return commands.getoutput ('git tag').split ('\n')