1 '''TODO: "import stgit"'''
7 from cStringIO
import StringIO
9 from PyQt4
.QtCore
import QProcess
10 from PyQt4
.QtCore
import QObject
13 # A regex for matching the output of git(log|rev-list) --pretty=oneline
14 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
17 return ' '.join([ utils
.shell_quote(arg
) for arg
in argv
])
19 def git(*args
,**kwargs
):
20 return run_cmd('git', *args
, **kwargs
)
22 def run_cmd(cmd
, *args
, **kwargs
):
23 # Handle cmd as either a string or an argv list
28 cmd
= list(cmd
+ list(args
))
31 child
.setProcessChannelMode(QProcess
.MergedChannels
);
32 child
.start(cmd
[0], cmd
[1:])
34 if(not child
.waitForStarted()):
35 raise Exception("failed to start child")
37 if(not child
.waitForFinished()):
38 raise Exception("failed to start child")
40 output
= str(child
.readAll())
42 # Allow run_cmd(argv, raw=True) for when we
43 # want the full, raw output(e.g. git cat-file)
44 if 'raw' in kwargs
and kwargs
['raw']:
47 if 'with_status' in kwargs
:
48 return child
.exitCode(), output
.rstrip()
50 return output
.rstrip()
53 '''Invokes 'git add' to index the filenames in to_add.'''
54 if not to_add
: return 'No files to add.'
55 argv
= [ 'git', 'add' ]
57 return quote(argv
) + '\n' + run_cmd(argv
)
59 def git_add_or_remove(to_process
):
60 '''Invokes 'git add' to index the filenames in to_process that exist
61 and 'git rm' for those that do not exist.'''
64 return 'No files to add or remove.'
69 for filename
in to_process
:
70 if os
.path
.exists(filename
):
71 to_add
.append(filename
)
75 if len(to_add
) == len(to_process
):
76 # to_process only contained unremoved files --
77 # short-circuit the removal checks
80 # Process files to remote
81 argv
= [ 'git', 'rm' ]
82 for filename
in to_process
:
83 if not os
.path
.exists(filename
):
89 def git_apply(filename
, indexonly
=True, reverse
=False):
90 argv
= ['git', 'apply']
91 if reverse
: argv
.append('--reverse')
92 if indexonly
: argv
.extend(['--index', '--cached'])
96 def git_branch(name
=None, remote
=False, delete
=False):
97 argv
= ['git', 'branch']
99 return run_cmd(argv
, '-D', name
)
101 if remote
: argv
.append('-r')
103 branches
= run_cmd(argv
).splitlines()
104 return map(lambda(x
): x
.lstrip('* '), branches
)
106 def git_cat_file(objtype
, sha1
):
107 cmd
= 'git cat-file %s %s' %( objtype
, sha1
)
108 return run_cmd(cmd
, raw
=True)
110 def git_cherry_pick(revs
, commit
=False):
111 '''Cherry-picks each revision into the current branch.'''
113 return 'No revision selected.'
115 argv
= [ 'git', 'cherry-pick' ]
116 if not commit
: argv
.append('-n')
120 output
.append('Cherry picking:' + ' '+ rev
)
121 output
.append(run_cmd(argv
, rev
))
123 return '\n'.join(output
)
125 def git_checkout(rev
):
126 return run_cmd('git','checkout', rev
)
128 def git_commit(msg
, amend
=False):
129 '''Creates a git commit. 'commit_all' triggers the -a
130 flag to 'git commit.' 'amend' triggers --amend.
131 'files' is a list of files to use for commits without -a.'''
133 # Sure, this is a potential "security risk," but if someone
134 # is trying to intercept/re-write commit messages on your system,
135 # then you probably have bigger problems to worry about.
136 tmpfile
= utils
.get_tmp_filename()
137 argv
= [ 'git', 'commit', '-F', tmpfile
]
138 if amend
: argv
.append('--amend')
140 # Create the commit message file
141 file = open(tmpfile
, 'w')
146 output
= run_cmd(argv
)
149 return quote(argv
) + '\n\n' + output
151 def git_create_branch(name
, base
, track
=False):
152 '''Creates a branch starting from base. Pass track=True
153 to create a remote tracking branch.'''
154 argv
= ['git','branch']
155 if track
: argv
.append('--track')
156 return run_cmd(argv
, name
, base
)
159 def git_current_branch():
160 '''Parses 'git branch' to find the current branch.'''
161 branches
= run_cmd('git branch').splitlines()
162 for branch
in branches
:
163 if branch
.startswith('* '):
164 return branch
.lstrip('* ')
166 return 'Detached HEAD'
168 def git_diff(commit
=None,filename
=None, color
=False,
169 cached
=True, with_diff_header
=False,
171 "Invokes git_diff on a filepath."
174 if reverse
: argv
.append('-R')
175 if color
: argv
.append('--color')
176 if cached
: argv
.append('--cached')
178 deleted
= cached
and not os
.path
.exists(filename
)
182 argv
.append(filename
)
185 argv
.append('%s^..%s' % (commit
,commit
))
188 diff_lines
= diff
.splitlines()
192 del_tag
= 'deleted file mode '
195 for line
in diff_lines
:
196 if not start
and '@@ ' in line
and ' @@' in line
:
198 if start
or(deleted
and del_tag
in line
):
199 output
.write(line
+ '\n')
203 result
= output
.getvalue()
207 return(os
.linesep
.join(headers
), result
)
212 '''Returns the latest diffstat.'''
213 return run_cmd('git diff --stat HEAD^')
215 def git_format_patch(revs
, use_range
):
216 '''Exports patches revs in the 'ugit-patches' subdirectory.
217 If use_range is True, a commit range is passed to git format-patch.'''
219 argv
= ['git','format-patch','--thread','--patch-with-stat',
224 header
= 'Generated Patches:'
226 rev_range
= '%s^..%s' %( revs
[-1], revs
[0] )
228 + run_cmd(argv
, rev_range
))
232 for idx
, rev
in enumerate(revs
):
233 real_idx
= str(idx
+ num_patches
)
235 run_cmd(argv
, '-1', '--start-number', real_idx
, rev
))
237 num_patches
+= output
[-1].count('\n')
239 return '\n'.join(output
)
241 def git_config(key
, value
=None):
242 '''Gets or sets git config values. If value is not None, then
243 the config key will be set. Otherwise, the config value of the
244 config key is returned.'''
245 if value
is not None:
246 return run_cmd('git', 'config', key
, value
)
248 return run_cmd('git', 'config', '--get', key
)
250 def git_log(oneline
=True, all
=False):
251 '''Returns a pair of parallel arrays listing the revision sha1's
252 and commit summaries.'''
253 argv
= [ 'git', 'log' ]
255 argv
.append('--pretty=oneline')
260 regex
= REV_LIST_REGEX
261 output
= run_cmd(argv
)
262 for line
in output
.splitlines():
263 match
= regex
.match(line
)
265 revs
.append(match
.group(1))
266 summaries
.append(match
.group(2))
267 return( revs
, summaries
)
270 return run_cmd('git ls-files').splitlines()
272 def git_ls_tree(rev
):
273 '''Returns a list of(mode, type, sha1, path) tuples.'''
275 lines
= run_cmd('git', 'ls-tree', '-r', rev
).splitlines()
277 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
279 match
= regex
.match(line
)
281 mode
= match
.group(1)
282 objtype
= match
.group(2)
283 sha1
= match
.group(3)
284 filename
= match
.group(4)
285 output
.append((mode
, objtype
, sha1
, filename
,) )
288 def git_push(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
289 argv
= ['git', 'push']
291 argv
.append('--tags')
294 if local_branch
== remote_branch
:
295 argv
.append(local_branch
)
297 if not ffwd
and local_branch
:
298 argv
.append('+%s:%s' % ( local_branch
, remote_branch
))
300 argv
.append('%s:%s' % ( local_branch
, remote_branch
))
302 return run_cmd(argv
, with_status
=True)
304 def git_rebase(newbase
):
305 if not newbase
: return
306 return run_cmd('git','rebase', newbase
)
308 def git_remote(*args
):
309 return run_cmd('git','remote',*args
).splitlines()
311 def git_remote_show(remote
):
313 for line
in git_remote('show',remote
):
314 info
.append(line
.strip())
317 def git_remote_url(remote
):
318 return utils
.grep('^URL:\s+(.*)', git_remote_show(remote
))
320 def git_reset(to_unstage
):
321 '''Use 'git reset' to unstage files from the index.'''
324 return 'No files to reset.'
326 argv
= [ 'git', 'reset', '--' ]
327 argv
.extend(to_unstage
)
329 return quote(argv
) + '\n' + run_cmd(argv
)
331 def git_rev_list_range(start
, end
):
333 argv
= [ 'git', 'rev-list', '--pretty=oneline', start
, end
]
335 raw_revs
= run_cmd(argv
).splitlines()
337 regex
= REV_LIST_REGEX
338 for line
in raw_revs
:
339 match
= regex
.match(line
)
341 rev_id
= match
.group(1)
342 summary
= match
.group(2)
343 revs
.append((rev_id
, summary
,) )
347 def git_show(sha1
, color
=False):
349 if color
: cmd
+= '--color '
350 return run_cmd(cmd
+ sha1
)
353 '''Returns a relative path to the git project root.'''
354 return run_cmd('git rev-parse --show-cdup')
357 '''RETURNS: A tuple of staged, unstaged and untracked files.
358 ( array(staged), array(unstaged), array(untracked) )'''
360 status_lines
= run_cmd('git status').splitlines()
362 unstaged_header_seen
= False
363 untracked_header_seen
= False
365 modified_header
= '# Changed but not updated:'
366 modified_regex
= re
.compile('(#\tmodified:\W{3}'
367 + '|#\tnew file:\W{3}'
368 + '|#\tdeleted:\W{4})')
370 renamed_regex
= re
.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
372 untracked_header
= '# Untracked files:'
373 untracked_regex
= re
.compile('#\t(.+)')
380 for status_line
in status_lines
:
381 if untracked_header
in status_line
:
382 untracked_header_seen
= True
384 if not untracked_header_seen
:
386 match
= untracked_regex
.match(status_line
)
388 filename
= match
.group(1)
389 untracked
.append(filename
)
391 # Staged, unstaged, and renamed files
392 for status_line
in status_lines
:
393 if modified_header
in status_line
:
394 unstaged_header_seen
= True
396 match
= modified_regex
.match(status_line
)
399 filename
= status_line
.replace(tag
, '')
400 if unstaged_header_seen
:
401 unstaged
.append(filename
)
403 staged
.append(filename
)
406 match
= renamed_regex
.match(status_line
)
408 oldname
= match
.group(2)
409 newname
= match
.group(3)
410 staged
.append(oldname
)
411 staged
.append(newname
)
413 return( staged
, unstaged
, untracked
)
416 return run_cmd('git tag').splitlines()