1 '''TODO: "import stgit"'''
7 from cStringIO
import StringIO
9 # A regex for matching the output of git(log|rev-list) --pretty=oneline
10 REV_LIST_REGEX
= re
.compile('([0-9a-f]+)\W(.*)')
13 return ' '.join([ utils
.shell_quote(arg
) for arg
in argv
])
15 def git(*args
,**kwargs
):
16 return utils
.run_cmd('git', *args
, **kwargs
)
19 '''Invokes 'git add' to index the filenames in to_add.'''
20 if not to_add
: return 'No files to add.'
21 return git('add', '-v', *to_add
)
23 def 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.'''
28 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
)
48 output
+ '\n\n' + git('rm',*to_remove
)
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')
63 branches
= map(lambda x
: x
.lstrip('* '),
64 git(*argv
).splitlines())
67 for branch
in branches
:
68 if branch
.endswith('/HEAD'): continue
69 remotes
.append(branch
)
73 def cat_file(objtype
, sha1
):
74 return git('cat-file', objtype
, sha1
, raw
=True)
76 def cherry_pick(revs
, commit
=False):
77 '''Cherry-picks each revision into the current branch.'''
79 return 'No revision selected.'
80 argv
= [ 'cherry-pick' ]
81 if not commit
: argv
.append('-n')
85 new_argv
= argv
+ [rev
]
86 cherries
.append(git(*new_argv
))
88 return '\n'.join(cherries
)
91 return git('checkout', rev
)
93 def commit(msg
, amend
=False):
94 '''Creates a git commit.'''
96 if not msg
.endswith('\n'):
99 # Sure, this is a potential "security risk," but if someone
100 # is trying to intercept/re-write commit messages on your system,
101 # then you probably have bigger problems to worry about.
102 tmpfile
= utils
.get_tmp_filename()
103 argv
= [ 'commit', '-F', tmpfile
]
105 argv
.append('--amend')
107 # Create the commit message file
108 file = open(tmpfile
, 'w')
116 return 'git ' + quote(argv
) + '\n\n' + output
118 def create_branch(name
, base
, track
=False):
119 '''Creates a branch starting from base. Pass track=True
120 to create a remote tracking branch.'''
121 if track
: return git('branch', '--track', name
, base
)
122 else: return git('branch', name
, base
)
124 def current_branch():
125 '''Parses 'git branch' to find the current branch.'''
126 branches
= git('branch').splitlines()
127 for branch
in branches
:
128 if branch
.startswith('* '):
129 return branch
.lstrip('* ')
130 return 'Detached HEAD'
132 def diff(commit
=None,filename
=None, color
=False,
133 cached
=True, with_diff_header
=False,
134 suppress_header
=True, reverse
=False):
135 "Invokes git diff on a filepath."
137 argv
= [ 'diff', '--unified='+str(defaults
.DIFF_CONTEXT
), '--patch-with-raw']
138 if reverse
: argv
.append('-R')
139 if color
: argv
.append('--color')
140 if cached
: argv
.append('--cached')
142 deleted
= cached
and not os
.path
.exists(filename
)
146 argv
.append(filename
)
149 argv
.append('%s^..%s' % (commit
,commit
))
152 diff_lines
= diff
.splitlines()
156 del_tag
= 'deleted file mode '
159 for line
in diff_lines
:
160 if not start
and '@@ ' in line
and ' @@' in line
:
162 if start
or(deleted
and del_tag
in line
):
163 output
.write(line
+ '\n')
167 elif not suppress_header
:
168 output
.write(line
+ '\n')
170 result
= output
.getvalue()
174 return('\n'.join(headers
), result
)
179 return git('diff','--unified='+str(defaults
.DIFF_CONTEXT
),
182 return git('diff', '--unified='+str(defaults
.DIFF_CONTEXT
),
183 '--stat', '--cached')
185 def format_patch(revs
):
186 '''writes patches named by revs to the "patches" directory.'''
189 argv
= ['format-patch','--thread','--patch-with-stat', '-o','patches']
192 for idx
, rev
in enumerate(revs
):
193 real_idx
= str(idx
+ num_patches
)
194 new_argv
= argv
+ ['--start-number', real_idx
,
196 output
.append(git(*new_argv
))
197 num_patches
+= output
[-1].count('\n')
198 return '\n'.join(output
)
200 def config(key
, value
=None, local
=True):
203 argv
.append('--global')
209 if type(value
) is bool:
210 value
= str(value
).lower()
211 argv
.append(str(value
))
214 def log(oneline
=True, all
=False):
215 '''Returns a pair of parallel arrays listing the revision sha1's
216 and commit summaries.'''
219 argv
.append('--pretty=oneline')
224 regex
= REV_LIST_REGEX
226 for line
in output
.splitlines():
227 match
= regex
.match(line
)
229 revs
.append(match
.group(1))
230 summaries
.append(match
.group(2))
231 return( revs
, summaries
)
234 return git('ls-files').splitlines()
237 '''Returns a list of(mode, type, sha1, path) tuples.'''
238 lines
= git('ls-tree', '-r', rev
).splitlines()
240 regex
= re
.compile('^(\d+)\W(\w+)\W(\w+)[ \t]+(.*)$')
242 match
= regex
.match(line
)
244 mode
= match
.group(1)
245 objtype
= match
.group(2)
246 sha1
= match
.group(3)
247 filename
= match
.group(4)
248 output
.append((mode
, objtype
, sha1
, filename
,) )
251 def push(remote
, local_branch
, remote_branch
, ffwd
=True, tags
=False):
254 argv
.append('--tags')
256 if local_branch
== remote_branch
:
257 argv
.append(local_branch
)
259 if not ffwd
and local_branch
:
260 argv
.append('+%s:%s' % ( local_branch
, remote_branch
))
262 argv
.append('%s:%s' % ( local_branch
, remote_branch
))
263 return git(with_status
=True, *argv
)
266 if not newbase
: return 'No base branch specified to rebase.'
267 return git('rebase', newbase
)
270 return git('remote', stderr
=False, *args
).splitlines()
272 def remote_url(name
):
273 return config('remote.%s.url' % name
)
275 def reset(to_unstage
):
276 '''Use 'git reset' to unstage files from the index.'''
278 return 'No files to reset.'
280 argv
= [ 'reset', '--' ]
281 argv
.extend(to_unstage
)
284 def rev_list_range(start
, end
):
285 argv
= [ 'rev-list', '--pretty=oneline', start
, end
]
286 raw_revs
= git(*argv
).splitlines()
288 for line
in raw_revs
:
289 match
= REV_LIST_REGEX
.match(line
)
291 rev_id
= match
.group(1)
292 summary
= match
.group(2)
293 revs
.append((rev_id
, summary
,) )
297 return git('show',sha1
)
300 '''Returns a relative path to the git project root.'''
301 return git('rev-parse','--show-cdup')
304 '''RETURNS: A tuple of staged, unstaged and untracked files.
305 ( array(staged), array(unstaged), array(untracked) )'''
307 status_lines
= git('status').splitlines()
309 unstaged_header_seen
= False
310 untracked_header_seen
= False
312 modified_header
= '# Changed but not updated:'
313 modified_regex
= re
.compile('(#\tmodified:\W{3}'
315 '|#\tdeleted:\W{4})')
317 renamed_regex
= re
.compile('(#\trenamed:\W{4})(.*?)\W->\W(.*)')
319 untracked_header
= '# Untracked files:'
320 untracked_regex
= re
.compile('#\t(.+)')
327 for status_line
in status_lines
:
328 if untracked_header
in status_line
:
329 untracked_header_seen
= True
331 if not untracked_header_seen
:
333 match
= untracked_regex
.match(status_line
)
335 filename
= match
.group(1)
336 untracked
.append(filename
)
338 # Staged, unstaged, and renamed files
339 for status_line
in status_lines
:
340 if modified_header
in status_line
:
341 unstaged_header_seen
= True
343 match
= modified_regex
.match(status_line
)
346 filename
= status_line
.replace(tag
, '')
347 if unstaged_header_seen
:
348 unstaged
.append(filename
)
350 staged
.append(filename
)
353 match
= renamed_regex
.match(status_line
)
355 oldname
= match
.group(2)
356 newname
= match
.group(3)
357 staged
.append(oldname
)
358 staged
.append(newname
)
360 return( staged
, unstaged
, untracked
)
363 return git('tag').splitlines()