1 '''TODO: "import stgit"'''
8 from cStringIO
import StringIO
10 # A regex for matching the output of git(log|rev-list) --pretty=oneline
11 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
14 return ' '.join([ utils
.shell_quote(arg
) for arg
in argv
])
16 def git(*args
,**kwargs
):
17 return utils
.run_cmd('git', *args
, **kwargs
)
20 '''Invokes 'git add' to index the filenames in to_add.'''
21 if not to_add
: return 'No files to add.'
22 return git('add', *to_add
)
24 def add_or_remove(to_process
):
25 '''Invokes 'git add' to index the filenames in to_process that exist
26 and 'git rm' for those that do not exist.'''
29 return 'No files to add or remove.'
35 for filename
in to_process
:
36 if os
.path
.exists(filename
):
37 to_add
.append(filename
)
41 if len(to_add
) == len(to_process
):
42 # to_process only contained unremoved files --
43 # short-circuit the removal checks
46 # Process files to remote
47 for filename
in to_process
:
48 if not os
.path
.exists(filename
):
49 to_remove
.append(filename
)
52 def apply(filename
, indexonly
=True, reverse
=False):
54 if reverse
: argv
.append('--reverse')
55 if indexonly
: argv
.extend(['--index', '--cached'])
59 def branch(name
=None, remote
=False, delete
=False):
61 return git('branch', '-D', name
)
64 if remote
: argv
.append('-r')
66 branches
= git(*argv
).splitlines()
67 return map(lambda(x
): x
.lstrip('* '), branches
)
69 def cat_file(objtype
, sha1
):
70 return git('cat-file', objtype
, sha1
, raw
=True)
72 def cherry_pick(revs
, commit
=False):
73 '''Cherry-picks each revision into the current branch.'''
75 return 'No revision selected.'
76 argv
= [ 'cherry-pick' ]
77 if not commit
: argv
.append('-n')
81 new_argv
= argv
+ [rev
]
82 cherries
.append(git(*new_argv
))
84 return os
.linesep
.join(cherries
)
87 return git('checkout', rev
)
89 def commit(msg
, amend
=False):
90 '''Creates a git commit.'''
92 if not msg
.endswith(os
.linesep
):
95 # Sure, this is a potential "security risk," but if someone
96 # is trying to intercept/re-write commit messages on your system,
97 # then you probably have bigger problems to worry about.
98 tmpfile
= utils
.get_tmp_filename()
99 argv
= [ 'commit', '-F', tmpfile
]
101 argv
.append('--amend')
103 # Create the commit message file
104 file = open(tmpfile
, 'w')
112 return quote(argv
) + os
.linesep
*2 + output
114 def create_branch(name
, base
, track
=False):
115 '''Creates a branch starting from base. Pass track=True
116 to create a remote tracking branch.'''
118 return git('branch', '--track', name
, base
)
120 return git('branch', name
, base
)
122 def current_branch():
123 '''Parses 'git branch' to find the current branch.'''
124 branches
= git('branch').splitlines()
125 for branch
in branches
:
126 if branch
.startswith('* '):
127 return branch
.lstrip('* ')
128 return 'Detached HEAD'
130 def diff(commit
=None,filename
=None, color
=False,
131 cached
=True, with_diff_header
=False,
132 suppress_header
=True, reverse
=False):
133 "Invokes git diff on a filepath."
135 argv
= [ 'diff', '--unified='+str(defaults
.DIFF_CONTEXT
), '--patch-with-raw']
136 if reverse
: argv
.append('-R')
137 if color
: argv
.append('--color')
138 if cached
: argv
.append('--cached')
140 deleted
= cached
and not os
.path
.exists(filename
)
144 argv
.append(filename
)
147 argv
.append('%s^..%s' % (commit
,commit
))
150 diff_lines
= diff
.splitlines()
154 del_tag
= 'deleted file mode '
157 for line
in diff_lines
:
158 if not start
and '@@ ' in line
and ' @@' in line
:
160 if start
or(deleted
and del_tag
in line
):
162 output
.write(os
.linesep
)
166 elif not suppress_header
:
168 output
.write(os
.linesep
)
170 result
= output
.getvalue()
174 return(os
.linesep
.join(headers
), result
)
179 return git('diff','--stat','HEAD^')
181 def format_patch(revs
):
182 '''writes patches named by revs to the "patches" directory.'''
185 argv
= ['format-patch','--thread','--patch-with-stat', '-o','patches']
188 for idx
, rev
in enumerate(revs
):
189 real_idx
= str(idx
+ num_patches
)
190 new_argv
= argv
+ ['--start-number', real_idx
,
192 output
.append(git(*new_argv
))
193 num_patches
+= output
[-1].count(os
.linesep
)
194 return os
.linesep
.join(output
)
196 def config(key
, value
=None, local
=True):
199 argv
.append('--global')
208 def log(oneline
=True, all
=False):
209 '''Returns a pair of parallel arrays listing the revision sha1's
210 and commit summaries.'''
213 argv
.append('--pretty=oneline')
218 regex
= REV_LIST_REGEX
220 for line
in output
.splitlines():
221 match
= regex
.match(line
)
223 revs
.append(match
.group(1))
224 summaries
.append(match
.group(2))
225 return( revs
, summaries
)
228 return git('ls-files').splitlines()
231 '''Returns a list of(mode, type, sha1, path) tuples.'''
232 lines
= git('ls-tree', '-r', rev
).splitlines()
234 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
236 match
= regex
.match(line
)
238 mode
= match
.group(1)
239 objtype
= match
.group(2)
240 sha1
= match
.group(3)
241 filename
= match
.group(4)
242 output
.append((mode
, objtype
, sha1
, filename
,) )
245 def push(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
248 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
))
257 return git(with_status
=True, *argv
)
260 if not newbase
: return
261 return git('rebase', newbase
)
264 return git('remote',*args
).splitlines()
266 def remote_url(name
):
267 return config('remote.%s.url' % name
)
269 def reset(to_unstage
):
270 '''Use 'git reset' to unstage files from the index.'''
272 return 'No files to reset.'
274 argv
= [ 'reset', '--' ]
275 argv
.extend(to_unstage
)
279 def rev_list_range(start
, end
):
280 argv
= [ 'rev-list', '--pretty=oneline', start
, end
]
281 raw_revs
= git(*argv
).splitlines()
283 for line
in raw_revs
:
284 match
= REV_LIST_REGEX
.match(line
)
286 rev_id
= match
.group(1)
287 summary
= match
.group(2)
288 revs
.append((rev_id
, summary
,) )
292 return git('show',sha1
)
295 '''Returns a relative path to the git project root.'''
296 return git('rev-parse','--show-cdup')
299 '''RETURNS: A tuple of staged, unstaged and untracked files.
300 ( array(staged), array(unstaged), array(untracked) )'''
302 status_lines
= git('status').splitlines()
304 unstaged_header_seen
= False
305 untracked_header_seen
= False
307 modified_header
= '# Changed but not updated:'
308 modified_regex
= re
.compile('(#\tmodified:\W{3}'
309 + '|#\tnew file:\W{3}'
310 + '|#\tdeleted:\W{4})')
312 renamed_regex
= re
.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
314 untracked_header
= '# Untracked files:'
315 untracked_regex
= re
.compile('#\t(.+)')
322 for status_line
in status_lines
:
323 if untracked_header
in status_line
:
324 untracked_header_seen
= True
326 if not untracked_header_seen
:
328 match
= untracked_regex
.match(status_line
)
330 filename
= match
.group(1)
331 untracked
.append(filename
)
333 # Staged, unstaged, and renamed files
334 for status_line
in status_lines
:
335 if modified_header
in status_line
:
336 unstaged_header_seen
= True
338 match
= modified_regex
.match(status_line
)
341 filename
= status_line
.replace(tag
, '')
342 if unstaged_header_seen
:
343 unstaged
.append(filename
)
345 staged
.append(filename
)
348 match
= renamed_regex
.match(status_line
)
350 oldname
= match
.group(2)
351 newname
= match
.group(3)
352 staged
.append(oldname
)
353 staged
.append(newname
)
355 return( staged
, unstaged
, untracked
)
358 return git('tag').splitlines()