1 '''TODO: "import stgit"'''
6 from cStringIO
import StringIO
8 from PyQt4
.QtCore
import QProcess
9 from PyQt4
.QtCore
import QObject
12 # A regex for matching the output of git(log|rev-list) --pretty=oneline
13 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
16 return ' '.join([ utils
.shell_quote(arg
) for arg
in argv
])
18 def git(*args
,**kwargs
):
19 return run_cmd('git', *args
, **kwargs
)
21 def run_cmd(cmd
, *args
, **kwargs
):
22 # Handle cmd as either a string or an argv list
27 cmd
= list(cmd
+ list(args
))
30 child
.setProcessChannelMode(QProcess
.MergedChannels
);
31 child
.start(cmd
[0], cmd
[1:])
33 if not child
.waitForStarted(): raise Exception("failed to start child")
34 if not child
.waitForFinished(): raise Exception("failed to start child")
36 output
= str(child
.readAll())
38 # Allow run_cmd(argv, raw=True) for when we
39 # want the full, raw output(e.g. git cat-file)
43 if 'with_status' in kwargs
:
44 return child
.exitCode(), output
.rstrip()
46 return output
.rstrip()
49 '''Invokes 'git add' to index the filenames in to_add.'''
50 if not to_add
: return 'No files to add.'
51 return git('add', *to_add
)
53 def add_or_remove(to_process
):
54 '''Invokes 'git add' to index the filenames in to_process that exist
55 and 'git rm' for those that do not exist.'''
58 return 'No files to add or remove.'
64 for filename
in to_process
:
65 if os
.path
.exists(filename
):
66 to_add
.append(filename
)
70 if len(to_add
) == len(to_process
):
71 # to_process only contained unremoved files --
72 # short-circuit the removal checks
75 # Process files to remote
76 for filename
in to_process
:
77 if not os
.path
.exists(filename
):
78 to_remove
.append(filename
)
81 def apply(filename
, indexonly
=True, reverse
=False):
83 if reverse
: argv
.append('--reverse')
84 if indexonly
: argv
.extend(['--index', '--cached'])
88 def branch(name
=None, remote
=False, delete
=False):
90 return git('branch', '-D', name
)
93 if remote
: argv
.append('-r')
95 branches
= git(*argv
).splitlines()
96 return map(lambda(x
): x
.lstrip('* '), branches
)
98 def cat_file(objtype
, sha1
):
99 return git('cat-file', objtype
, sha1
, raw
=True)
101 def cherry_pick(revs
, commit
=False):
102 '''Cherry-picks each revision into the current branch.'''
104 return 'No revision selected.'
105 argv
= [ 'cherry-pick' ]
106 if not commit
: argv
.append('-n')
110 new_argv
= argv
+ [rev
]
111 cherries
.append(git(*new_argv
))
113 return os
.linesep
.join(cherries
)
116 return git('checkout', rev
)
118 def commit(msg
, amend
=False):
119 '''Creates a git commit.'''
121 if not msg
.endswith(os
.linesep
):
124 # Sure, this is a potential "security risk," but if someone
125 # is trying to intercept/re-write commit messages on your system,
126 # then you probably have bigger problems to worry about.
127 tmpfile
= utils
.get_tmp_filename()
128 argv
= [ 'commit', '-F', tmpfile
]
130 argv
.append('--amend')
132 # Create the commit message file
133 file = open(tmpfile
, 'w')
141 return quote(argv
) + os
.linesep
*2 + output
143 def create_branch(name
, base
, track
=False):
144 '''Creates a branch starting from base. Pass track=True
145 to create a remote tracking branch.'''
147 return git('branch', '--track', name
, base
)
149 return git('branch', name
, base
)
151 def current_branch():
152 '''Parses 'git branch' to find the current branch.'''
153 branches
= git('branch').splitlines()
154 for branch
in branches
:
155 if branch
.startswith('* '):
156 return branch
.lstrip('* ')
157 return 'Detached HEAD'
159 def diff(commit
=None,filename
=None, color
=False,
160 cached
=True, with_diff_header
=False,
162 "Invokes git diff on a filepath."
165 if reverse
: argv
.append('-R')
166 if color
: argv
.append('--color')
167 if cached
: argv
.append('--cached')
169 deleted
= cached
and not os
.path
.exists(filename
)
173 argv
.append(filename
)
176 argv
.append('%s^..%s' % (commit
,commit
))
179 diff_lines
= diff
.splitlines()
183 del_tag
= 'deleted file mode '
186 for line
in diff_lines
:
187 if not start
and '@@ ' in line
and ' @@' in line
:
189 if start
or(deleted
and del_tag
in line
):
190 output
.write(line
+ '\n')
194 result
= output
.getvalue()
198 return(os
.linesep
.join(headers
), result
)
203 '''Returns the latest diffstat.'''
204 return git('diff','--stat','HEAD^')
206 def format_patch(revs
, use_range
):
207 '''Exports patches revs in the 'ugit-patches' subdirectory.
208 If use_range is True, a commit range is passed to git format-patch.'''
210 argv
= ['format-patch','--thread','--patch-with-stat',
214 header
= 'Generated Patches:'
216 new_argv
= argv
+ ['%s^..%s' %( revs
[-1], revs
[0] )]
217 return git(*new_argv
)
221 for idx
, rev
in enumerate(revs
):
222 real_idx
= str(idx
+ num_patches
)
223 new_argv
= argv
+ ['-1', '--start-number', real_idx
, rev
]
224 output
.append(git(*new_argv
))
225 num_patches
+= output
[-1].count(os
.linesep
)
226 return os
.linesep
.join(output
)
228 def config(key
, value
=None):
229 '''Gets or sets git config values. If value is not None, then
230 the config key will be set. Otherwise, the config value of the
231 config key is returned.'''
232 if value
is not None:
233 return git('config', key
, value
)
235 return git('config', '--get', key
)
237 def log(oneline
=True, all
=False):
238 '''Returns a pair of parallel arrays listing the revision sha1's
239 and commit summaries.'''
242 argv
.append('--pretty=oneline')
247 regex
= REV_LIST_REGEX
249 for line
in output
.splitlines():
250 match
= regex
.match(line
)
252 revs
.append(match
.group(1))
253 summaries
.append(match
.group(2))
254 return( revs
, summaries
)
257 return git('ls-files').splitlines()
260 '''Returns a list of(mode, type, sha1, path) tuples.'''
262 lines
= git('ls-tree', '-r', rev
).splitlines()
264 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
266 match
= regex
.match(line
)
268 mode
= match
.group(1)
269 objtype
= match
.group(2)
270 sha1
= match
.group(3)
271 filename
= match
.group(4)
272 output
.append((mode
, objtype
, sha1
, filename
,) )
275 def push(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
278 argv
.append('--tags')
281 if local_branch
== remote_branch
:
282 argv
.append(local_branch
)
284 if not ffwd
and local_branch
:
285 argv
.append('+%s:%s' % ( local_branch
, remote_branch
))
287 argv
.append('%s:%s' % ( local_branch
, remote_branch
))
289 return git(with_status
=True, *argv
)
292 if not newbase
: return
293 return git('rebase', newbase
)
296 argv
= ['remote'] + list(args
)
297 return git(*argv
).splitlines()
299 def remote_show(name
):
300 return [ line
.strip() for line
in remote('show', name
) ]
302 def remote_url(name
):
303 return utils
.grep('^URL:\s+(.*)', remote_show(name
))
305 def reset(to_unstage
):
306 '''Use 'git reset' to unstage files from the index.'''
308 return 'No files to reset.'
310 argv
= [ 'reset', '--' ]
311 argv
.extend(to_unstage
)
315 def rev_list_range(start
, end
):
316 argv
= [ 'rev-list', '--pretty=oneline', start
, end
]
317 raw_revs
= git(*argv
).splitlines()
319 for line
in raw_revs
:
320 match
= REV_LIST_REGEX
.match(line
)
322 rev_id
= match
.group(1)
323 summary
= match
.group(2)
324 revs
.append((rev_id
, summary
,) )
327 def show(sha1
, color
=False):
329 if color
: cmd
+= '--color '
330 return run_cmd(cmd
+ sha1
)
333 '''Returns a relative path to the git project root.'''
334 return git('rev-parse','--show-cdup')
337 '''RETURNS: A tuple of staged, unstaged and untracked files.
338 ( array(staged), array(unstaged), array(untracked) )'''
340 status_lines
= git('status').splitlines()
342 unstaged_header_seen
= False
343 untracked_header_seen
= False
345 modified_header
= '# Changed but not updated:'
346 modified_regex
= re
.compile('(#\tmodified:\W{3}'
347 + '|#\tnew file:\W{3}'
348 + '|#\tdeleted:\W{4})')
350 renamed_regex
= re
.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
352 untracked_header
= '# Untracked files:'
353 untracked_regex
= re
.compile('#\t(.+)')
360 for status_line
in status_lines
:
361 if untracked_header
in status_line
:
362 untracked_header_seen
= True
364 if not untracked_header_seen
:
366 match
= untracked_regex
.match(status_line
)
368 filename
= match
.group(1)
369 untracked
.append(filename
)
371 # Staged, unstaged, and renamed files
372 for status_line
in status_lines
:
373 if modified_header
in status_line
:
374 unstaged_header_seen
= True
376 match
= modified_regex
.match(status_line
)
379 filename
= status_line
.replace(tag
, '')
380 if unstaged_header_seen
:
381 unstaged
.append(filename
)
383 staged
.append(filename
)
386 match
= renamed_regex
.match(status_line
)
388 oldname
= match
.group(2)
389 newname
= match
.group(3)
390 staged
.append(oldname
)
391 staged
.append(newname
)
393 return( staged
, unstaged
, untracked
)
396 return git('tag').splitlines()