43bdc7ee8e37f81c565f6c27d30dbd0c4525d355
1 """Python GIT interface
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 import sys
, os
, popen2
, re
, gitmergeonefile
23 from stgit
import basedir
24 from stgit
.utils
import *
25 from stgit
.config
import config
28 class GitException(Exception):
37 """Handle the commit objects
39 def __init__(self
, id_hash
):
40 self
.__id_hash
= id_hash
42 lines
= _output_lines('git-cat-file commit %s' % id_hash
)
43 for i
in range(len(lines
)):
47 field
= line
.strip().split(' ', 1)
48 if field
[0] == 'tree':
49 self
.__tree
= field
[1]
50 if field
[0] == 'author':
51 self
.__author
= field
[1]
52 if field
[0] == 'committer':
53 self
.__committer
= field
[1]
54 self
.__log
= ''.join(lines
[i
+1:])
56 def get_id_hash(self
):
63 parents
= self
.get_parents()
69 def get_parents(self
):
70 return _output_lines('git-rev-list --parents --max-count=1 %s'
71 % self
.__id_hash
)[0].split()[1:]
76 def get_committer(self
):
77 return self
.__committer
82 # dictionary of Commit objects, used to avoid multiple calls to git
89 def get_commit(id_hash
):
90 """Commit objects factory. Save/look-up them in the __commits
95 if id_hash
in __commits
:
96 return __commits
[id_hash
]
98 commit
= Commit(id_hash
)
99 __commits
[id_hash
] = commit
103 """Return the list of file conflicts
105 conflicts_file
= os
.path
.join(basedir
.get(), 'conflicts')
106 if os
.path
.isfile(conflicts_file
):
107 f
= file(conflicts_file
)
108 names
= [line
.strip() for line
in f
.readlines()]
114 def _input(cmd
, file_desc
):
115 p
= popen2
.Popen3(cmd
, True)
117 line
= file_desc
.readline()
120 p
.tochild
.write(line
)
123 raise GitException
, '%s failed (%s)' %
(str(cmd
),
124 p
.childerr
.read().strip())
126 def _input_str(cmd
, string
):
127 p
= popen2
.Popen3(cmd
, True)
128 p
.tochild
.write(string
)
131 raise GitException
, '%s failed (%s)' %
(str(cmd
),
132 p
.childerr
.read().strip())
135 p
=popen2
.Popen3(cmd
, True)
136 output
= p
.fromchild
.read()
138 raise GitException
, '%s failed (%s)' %
(str(cmd
),
139 p
.childerr
.read().strip())
142 def _output_one_line(cmd
, file_desc
= None):
143 p
=popen2
.Popen3(cmd
, True)
144 if file_desc
!= None:
145 for line
in file_desc
:
146 p
.tochild
.write(line
)
148 output
= p
.fromchild
.readline().strip()
150 raise GitException
, '%s failed (%s)' %
(str(cmd
),
151 p
.childerr
.read().strip())
154 def _output_lines(cmd
):
155 p
=popen2
.Popen3(cmd
, True)
156 lines
= p
.fromchild
.readlines()
158 raise GitException
, '%s failed (%s)' %
(str(cmd
),
159 p
.childerr
.read().strip())
162 def __run(cmd
, args
=None):
163 """__run: runs cmd using spawnvp.
165 Runs cmd using spawnvp. The shell is avoided so it won't mess up
166 our arguments. If args is very large, the command is run multiple
167 times; args is split xargs style: cmd is passed on each
168 invocation. Unlike xargs, returns immediately if any non-zero
169 return code is received.
175 for i
in range(0, len(args
)+1, 100):
176 r
=os
.spawnvp(os
.P_WAIT
, args_l
[0], args_l
+ args
[i
:min(i
+100, len(args
))])
181 def __tree_status(files
= None, tree_id
= 'HEAD', unknown
= False,
183 """Returns a list of pairs - [status, filename]
193 exclude_file
= os
.path
.join(basedir
.get(), 'info', 'exclude')
194 base_exclude
= ['--exclude=%s' % s for s in
195 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
196 base_exclude
.append('--exclude-per-directory=.gitignore')
198 if os
.path
.exists(exclude_file
):
199 extra_exclude
= ['--exclude-from=%s' % exclude_file
]
203 extra_exclude
= base_exclude
= []
205 lines
= _output_lines(['git-ls-files', '--others', '--directory']
206 + base_exclude
+ extra_exclude
)
207 cache_files
+= [('?', line
.strip()) for line
in lines
]
210 conflicts
= get_conflicts()
213 cache_files
+= [('C', filename
) for filename
in conflicts
]
216 for line
in _output_lines(['git-diff-index', tree_id
, '--'] + files
):
217 fs
= tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
218 if fs
[1] not in conflicts
:
219 cache_files
.append(fs
)
224 """Return true if there are local changes in the tree
226 return len(__tree_status()) != 0
232 """Verifies the HEAD and returns the SHA1 id that represents it
237 __head
= rev_parse('HEAD')
241 """Returns the name of the file pointed to by the HEAD link
243 return strip_prefix('refs/heads/',
244 _output_one_line('git-symbolic-ref HEAD'))
246 def set_head_file(ref
):
247 """Resets HEAD to point to a new ref
249 # head cache flushing is needed since we might have a different value
252 if __run('git-symbolic-ref HEAD',
253 [os
.path
.join('refs', 'heads', ref
)]) != 0:
254 raise GitException
, 'Could not set head to "%s"' % ref
257 """Sets the HEAD value
261 if not __head
or __head
!= val
:
262 if __run('git-update-ref HEAD', [val
]) != 0:
263 raise GitException
, 'Could not update HEAD to "%s".' % val
266 # only allow SHA1 hashes
267 assert(len(__head
) == 40)
269 def __clear_head_cache():
270 """Sets the __head to None so that a re-read is forced
277 """Refresh index with stat() information from the working directory.
279 __run('git-update-index -q --unmerged --refresh')
281 def rev_parse(git_id
):
282 """Parse the string and return a verified SHA1 id
285 return _output_one_line(['git-rev-parse', '--verify', git_id
])
287 raise GitException
, 'Unknown revision: %s' % git_id
289 def branch_exists(branch
):
290 """Existence check for the named branch
292 branch
= os
.path
.join('refs', 'heads', branch
)
293 for line
in _output_lines('git-rev-parse --symbolic --all 2>&1'):
294 if line
.strip() == branch
:
296 if re
.compile('[ |/]'+branch
+' ').search(line
):
297 raise GitException
, 'Bogus branch: %s' % line
300 def create_branch(new_branch
, tree_id
= None):
301 """Create a new branch in the git repository
303 if branch_exists(new_branch
):
304 raise GitException
, 'Branch "%s" already exists' % new_branch
306 current_head
= get_head()
307 set_head_file(new_branch
)
308 __set_head(current_head
)
310 # a checkout isn't needed if new branch points to the current head
314 if os
.path
.isfile(os
.path
.join(basedir
.get(), 'MERGE_HEAD')):
315 os
.remove(os
.path
.join(basedir
.get(), 'MERGE_HEAD'))
317 def switch_branch(new_branch
):
318 """Switch to a git branch
322 if not branch_exists(new_branch
):
323 raise GitException
, 'Branch "%s" does not exist' % new_branch
325 tree_id
= rev_parse(os
.path
.join('refs', 'heads', new_branch
)
327 if tree_id
!= get_head():
329 if __run('git-read-tree -u -m', [get_head(), tree_id
]) != 0:
330 raise GitException
, 'git-read-tree failed (local changes maybe?)'
332 set_head_file(new_branch
)
334 if os
.path
.isfile(os
.path
.join(basedir
.get(), 'MERGE_HEAD')):
335 os
.remove(os
.path
.join(basedir
.get(), 'MERGE_HEAD'))
337 def delete_branch(name
):
338 """Delete a git branch
340 if not branch_exists(name
):
341 raise GitException
, 'Branch "%s" does not exist' % name
342 remove_file_and_dirs(os
.path
.join(basedir
.get(), 'refs', 'heads'),
345 def rename_branch(from_name
, to_name
):
346 """Rename a git branch
348 if not branch_exists(from_name
):
349 raise GitException
, 'Branch "%s" does not exist' % from_name
350 if branch_exists(to_name
):
351 raise GitException
, 'Branch "%s" already exists' % to_name
353 if get_head_file() == from_name
:
354 set_head_file(to_name
)
355 rename(os
.path
.join(basedir
.get(), 'refs', 'heads'),
359 """Add the files or recursively add the directory contents
361 # generate the file list
364 if not os
.path
.exists(i
):
365 raise GitException
, 'Unknown file or directory: %s' % i
368 # recursive search. We only add files
369 for root
, dirs
, local_files
in os
.walk(i
):
370 for name
in [os
.path
.join(root
, f
) for f
in local_files
]:
371 if os
.path
.isfile(name
):
372 files
.append(os
.path
.normpath(name
))
373 elif os
.path
.isfile(i
):
374 files
.append(os
.path
.normpath(i
))
376 raise GitException
, '%s is not a file or directory' % i
379 if __run('git-update-index --add --', files
):
380 raise GitException
, 'Unable to add file'
382 def rm(files
, force
= False):
383 """Remove a file from the repository
387 if os
.path
.exists(f
):
388 raise GitException
, '%s exists. Remove it first' %f
390 __run('git-update-index --remove --', files
)
393 __run('git-update-index --force-remove --', files
)
395 def update_cache(files
= None, force
= False):
396 """Update the cache information for the given files
401 cache_files
= __tree_status(files
)
403 # everything is up-to-date
404 if len(cache_files
) == 0:
407 # check for unresolved conflicts
408 if not force
and [x
for x
in cache_files
409 if x
[0] not in ['M', 'N', 'A', 'D']]:
410 raise GitException
, 'Updating cache failed: unresolved conflicts'
413 add_files
= [x
[1] for x
in cache_files
if x
[0] in ['N', 'A']]
414 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['D']]
415 m_files
= [x
[1] for x
in cache_files
if x
[0] in ['M']]
417 if add_files
and __run('git-update-index --add --', add_files
) != 0:
418 raise GitException
, 'Failed git-update-index --add'
419 if rm_files
and __run('git-update-index --force-remove --', rm_files
) != 0:
420 raise GitException
, 'Failed git-update-index --rm'
421 if m_files
and __run('git-update-index --', m_files
) != 0:
422 raise GitException
, 'Failed git-update-index'
426 def commit(message
, files
= None, parents
= None, allowempty
= False,
427 cache_update
= True, tree_id
= None,
428 author_name
= None, author_email
= None, author_date
= None,
429 committer_name
= None, committer_email
= None):
430 """Commit the current tree to repository
437 # Get the tree status
438 if cache_update
and parents
!= []:
439 changes
= update_cache(files
)
440 if not changes
and not allowempty
:
441 raise GitException
, 'No changes to commit'
443 # get the commit message
446 elif message
[-1:] != '\n':
450 # write the index to repository
452 tree_id
= _output_one_line('git-write-tree')
459 cmd
+= 'GIT_AUTHOR_NAME="%s" ' % author_name
461 cmd
+= 'GIT_AUTHOR_EMAIL="%s" ' % author_email
463 cmd
+= 'GIT_AUTHOR_DATE="%s" ' % author_date
465 cmd
+= 'GIT_COMMITTER_NAME="%s" ' % committer_name
467 cmd
+= 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
468 cmd
+= 'git-commit-tree %s' % tree_id
474 commit_id
= _output_one_line(cmd
, message
)
476 __set_head(commit_id
)
480 def apply_diff(rev1
, rev2
, check_index
= True, files
= None):
481 """Apply the diff between rev1 and rev2 onto the current
482 index. This function doesn't need to raise an exception since it
483 is only used for fast-pushing a patch. If this operation fails,
484 the pushing would fall back to the three-way merge.
487 index_opt
= '--index'
494 diff_str
= diff(files
, rev1
, rev2
)
497 _input_str('git-apply %s' % index_opt
, diff_str
)
503 def merge(base
, head1
, head2
):
504 """Perform a 3-way merge between base, head1 and head2 into the
508 if __run('git-read-tree -u -m --aggressive', [base
, head1
, head2
]) != 0:
509 raise GitException
, 'git-read-tree failed (local changes maybe?)'
511 # check the index for unmerged entries
513 stages_re
= re
.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re
.S
)
515 for line
in _output('git-ls-files --unmerged --stage -z').split('\0'):
519 mode
, hash, stage
, path
= stages_re
.findall(line
)[0]
521 if not path
in files
:
523 files
[path
]['1'] = ('', '')
524 files
[path
]['2'] = ('', '')
525 files
[path
]['3'] = ('', '')
527 files
[path
][stage
] = (mode
, hash)
529 # merge the unmerged files
533 if gitmergeonefile
.merge(stages
['1'][1], stages
['2'][1],
534 stages
['3'][1], path
, stages
['1'][0],
535 stages
['2'][0], stages
['3'][0]) != 0:
539 raise GitException
, 'GIT index merging failed (possible conflicts)'
541 def status(files
= None, modified
= False, new
= False, deleted
= False,
542 conflict
= False, unknown
= False, noexclude
= False):
543 """Show the tree status
548 cache_files
= __tree_status(files
, unknown
= True, noexclude
= noexclude
)
549 all
= not (modified
or new
or deleted
or conflict
or unknown
)
564 cache_files
= [x
for x
in cache_files
if x
[0] in filestat
]
566 for fs
in cache_files
:
568 print '%s %s' %
(fs
[0], fs
[1])
572 def diff(files
= None, rev1
= 'HEAD', rev2
= None, out_fd
= None):
573 """Show the diff between rev1 and rev2
579 diff_str
= _output(['git-diff-tree', '-p', rev1
, rev2
, '--'] + files
)
583 diff_str
= _output(['git-diff-index', '-p', '-R', rev2
, '--'] + files
)
585 diff_str
= _output(['git-diff-index', '-p', rev1
, '--'] + files
)
590 out_fd
.write(diff_str
)
594 def diffstat(files
= None, rev1
= 'HEAD', rev2
= None):
595 """Return the diffstat between rev1 and rev2
600 p
=popen2
.Popen3('git-apply --stat')
601 diff(files
, rev1
, rev2
, p
.tochild
)
603 diff_str
= p
.fromchild
.read().rstrip()
605 raise GitException
, 'git.diffstat failed'
608 def files(rev1
, rev2
):
609 """Return the files modified between rev1 and rev2
613 for line
in _output_lines('git-diff-tree -r %s %s' %
(rev1
, rev2
)):
614 result
+= '%s %s\n' %
tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
616 return result
.rstrip()
618 def barefiles(rev1
, rev2
):
619 """Return the files modified between rev1 and rev2, without status info
623 for line
in _output_lines('git-diff-tree -r %s %s' %
(rev1
, rev2
)):
624 result
+= '%s\n' % line
.rstrip().split(' ',4)[-1].split('\t',1)[-1]
626 return result
.rstrip()
628 def pretty_commit(commit_id
= 'HEAD'):
629 """Return a given commit (log + diff)
631 return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
634 def checkout(files
= None, tree_id
= None, force
= False):
635 """Check out the given or all files
640 if tree_id
and __run('git-read-tree --reset', [tree_id
]) != 0:
641 raise GitException
, 'Failed git-read-tree --reset %s' % tree_id
643 checkout_cmd
= 'git-checkout-index -q -u'
645 checkout_cmd
+= ' -f'
647 checkout_cmd
+= ' -a'
649 checkout_cmd
+= ' --'
651 if __run(checkout_cmd
, files
) != 0:
652 raise GitException
, 'Failed git-checkout-index'
654 def switch(tree_id
, keep
= False):
655 """Switch the tree to the given id
659 if __run('git-read-tree -u -m', [get_head(), tree_id
]) != 0:
660 raise GitException
, 'git-read-tree failed (local changes maybe?)'
664 def reset(files
= None, tree_id
= None, check_out
= True):
665 """Revert the tree changes relative to the given tree_id. It removes
672 cache_files
= __tree_status(files
, tree_id
)
673 # files which were added but need to be removed
674 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['A']]
676 checkout(files
, tree_id
, True)
677 # checkout doesn't remove files
678 map(os
.remove
, rm_files
)
680 # if the reset refers to the whole tree, switch the HEAD as well
684 def pull(repository
= 'origin', refspec
= None):
685 """Pull changes from the remote repository. At the moment, just
686 use the 'git-pull' command
688 # 'git-pull' updates the HEAD
695 if __run(config
.get('stgit', 'pullcmd'), args
) != 0:
696 raise GitException
, 'Failed "git-pull %s"' % repository
698 def apply_patch(filename
= None, base
= None):
699 """Apply a patch onto the current or given index. There must not
700 be any local changes in the tree, otherwise the command fails
704 return __run('git-apply --index', [filename
]) == 0
707 _input('git-apply --index', sys
.stdin
)
713 orig_head
= get_head()
716 refresh_index() # needed since __apply_patch() doesn't do it
718 if not __apply_patch():
721 raise GitException
, 'Patch does not apply cleanly'
723 top
= commit(message
= 'temporary commit used for applying a patch',
726 merge(base
, orig_head
, top
)
728 def clone(repository
, local_dir
):
729 """Clone a remote repository. At the moment, just use the
732 if __run('git-clone', [repository
, local_dir
]) != 0:
733 raise GitException
, 'Failed "git-clone %s %s"' \
734 %
(repository
, local_dir
)
736 def modifying_revs(files
, base_rev
):
737 """Return the revisions from the list modifying the given files
739 cmd
= ['git-rev-list', '%s..' % base_rev
, '--']
740 revs
= [line
.strip() for line
in _output_lines(cmd
+ files
)]