e30b959e6fcb070fe8a8d4ffd922f676a071e469
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):
38 """An author, committer, etc."""
39 def __init__(self
, name
= None, email
= None, date
= '',
41 if name
or email
or date
:
47 assert not (name
or email
or date
)
49 m
= re
.match(r
'^(.+)<(.+)>(.*)$', s
)
51 return [x
.strip() or None for x
in m
.groups()]
52 self
.name
, self
.email
, self
.date
= parse_desc(desc
)
53 def set_name(self
, val
):
56 def set_email(self
, val
):
59 def set_date(self
, val
):
63 if self
.name
and self
.email
:
64 return '%s <%s>' %
(self
.name
, self
.email
)
66 raise GitException
, 'not enough identity data'
69 """Handle the commit objects
71 def __init__(self
, id_hash
):
72 self
.__id_hash
= id_hash
74 lines
= _output_lines('git-cat-file commit %s' % id_hash
)
75 for i
in range(len(lines
)):
79 field
= line
.strip().split(' ', 1)
80 if field
[0] == 'tree':
81 self
.__tree
= field
[1]
82 if field
[0] == 'author':
83 self
.__author
= field
[1]
84 if field
[0] == 'committer':
85 self
.__committer
= field
[1]
86 self
.__log
= ''.join(lines
[i
+1:])
88 def get_id_hash(self
):
95 parents
= self
.get_parents()
101 def get_parents(self
):
102 return _output_lines('git-rev-list --parents --max-count=1 %s'
103 % self
.__id_hash
)[0].split()[1:]
105 def get_author(self
):
108 def get_committer(self
):
109 return self
.__committer
115 return self
.get_id_hash()
117 # dictionary of Commit objects, used to avoid multiple calls to git
124 def get_commit(id_hash
):
125 """Commit objects factory. Save/look-up them in the __commits
130 if id_hash
in __commits
:
131 return __commits
[id_hash
]
133 commit
= Commit(id_hash
)
134 __commits
[id_hash
] = commit
138 """Return the list of file conflicts
140 conflicts_file
= os
.path
.join(basedir
.get(), 'conflicts')
141 if os
.path
.isfile(conflicts_file
):
142 f
= file(conflicts_file
)
143 names
= [line
.strip() for line
in f
.readlines()]
149 def _input(cmd
, file_desc
):
150 p
= popen2
.Popen3(cmd
, True)
152 line
= file_desc
.readline()
155 p
.tochild
.write(line
)
158 raise GitException
, '%s failed (%s)' %
(str(cmd
),
159 p
.childerr
.read().strip())
161 def _input_str(cmd
, string
):
162 p
= popen2
.Popen3(cmd
, True)
163 p
.tochild
.write(string
)
166 raise GitException
, '%s failed (%s)' %
(str(cmd
),
167 p
.childerr
.read().strip())
170 p
=popen2
.Popen3(cmd
, True)
171 output
= p
.fromchild
.read()
173 raise GitException
, '%s failed (%s)' %
(str(cmd
),
174 p
.childerr
.read().strip())
177 def _output_one_line(cmd
, file_desc
= None):
178 p
=popen2
.Popen3(cmd
, True)
179 if file_desc
!= None:
180 for line
in file_desc
:
181 p
.tochild
.write(line
)
183 output
= p
.fromchild
.readline().strip()
185 raise GitException
, '%s failed (%s)' %
(str(cmd
),
186 p
.childerr
.read().strip())
189 def _output_lines(cmd
):
190 p
=popen2
.Popen3(cmd
, True)
191 lines
= p
.fromchild
.readlines()
193 raise GitException
, '%s failed (%s)' %
(str(cmd
),
194 p
.childerr
.read().strip())
197 def __run(cmd
, args
=None):
198 """__run: runs cmd using spawnvp.
200 Runs cmd using spawnvp. The shell is avoided so it won't mess up
201 our arguments. If args is very large, the command is run multiple
202 times; args is split xargs style: cmd is passed on each
203 invocation. Unlike xargs, returns immediately if any non-zero
204 return code is received.
210 for i
in range(0, len(args
)+1, 100):
211 r
=os
.spawnvp(os
.P_WAIT
, args_l
[0], args_l
+ args
[i
:min(i
+100, len(args
))])
216 def __tree_status(files
= None, tree_id
= 'HEAD', unknown
= False,
217 noexclude
= True, verbose
= False):
218 """Returns a list of pairs - [status, filename]
220 if verbose
and sys
.stdout
.isatty():
221 print 'Checking for changes in the working directory...',
232 exclude_file
= os
.path
.join(basedir
.get(), 'info', 'exclude')
233 base_exclude
= ['--exclude=%s' % s for s in
234 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
235 base_exclude
.append('--exclude-per-directory=.gitignore')
237 if os
.path
.exists(exclude_file
):
238 extra_exclude
= ['--exclude-from=%s' % exclude_file
]
242 extra_exclude
= base_exclude
= []
244 lines
= _output_lines(['git-ls-files', '--others', '--directory']
245 + base_exclude
+ extra_exclude
)
246 cache_files
+= [('?', line
.strip()) for line
in lines
]
249 conflicts
= get_conflicts()
252 cache_files
+= [('C', filename
) for filename
in conflicts
]
255 for line
in _output_lines(['git-diff-index', tree_id
, '--'] + files
):
256 fs
= tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
257 if fs
[1] not in conflicts
:
258 cache_files
.append(fs
)
260 if verbose
and sys
.stdout
.isatty():
265 def local_changes(verbose
= True):
266 """Return true if there are local changes in the tree
268 return len(__tree_status(verbose
= verbose
)) != 0
274 """Verifies the HEAD and returns the SHA1 id that represents it
279 __head
= rev_parse('HEAD')
283 """Returns the name of the file pointed to by the HEAD link
285 return strip_prefix('refs/heads/',
286 _output_one_line('git-symbolic-ref HEAD'))
288 def set_head_file(ref
):
289 """Resets HEAD to point to a new ref
291 # head cache flushing is needed since we might have a different value
294 if __run('git-symbolic-ref HEAD',
295 [os
.path
.join('refs', 'heads', ref
)]) != 0:
296 raise GitException
, 'Could not set head to "%s"' % ref
299 """Sets the HEAD value
303 if not __head
or __head
!= val
:
304 if __run('git-update-ref HEAD', [val
]) != 0:
305 raise GitException
, 'Could not update HEAD to "%s".' % val
308 # only allow SHA1 hashes
309 assert(len(__head
) == 40)
311 def __clear_head_cache():
312 """Sets the __head to None so that a re-read is forced
319 """Refresh index with stat() information from the working directory.
321 __run('git-update-index -q --unmerged --refresh')
323 def rev_parse(git_id
):
324 """Parse the string and return a verified SHA1 id
327 return _output_one_line(['git-rev-parse', '--verify', git_id
])
329 raise GitException
, 'Unknown revision: %s' % git_id
331 def branch_exists(branch
):
332 """Existence check for the named branch
334 branch
= os
.path
.join('refs', 'heads', branch
)
335 for line
in _output_lines('git-rev-parse --symbolic --all 2>&1'):
336 if line
.strip() == branch
:
338 if re
.compile('[ |/]'+branch
+' ').search(line
):
339 raise GitException
, 'Bogus branch: %s' % line
342 def create_branch(new_branch
, tree_id
= None):
343 """Create a new branch in the git repository
345 if branch_exists(new_branch
):
346 raise GitException
, 'Branch "%s" already exists' % new_branch
348 current_head
= get_head()
349 set_head_file(new_branch
)
350 __set_head(current_head
)
352 # a checkout isn't needed if new branch points to the current head
356 if os
.path
.isfile(os
.path
.join(basedir
.get(), 'MERGE_HEAD')):
357 os
.remove(os
.path
.join(basedir
.get(), 'MERGE_HEAD'))
359 def switch_branch(new_branch
):
360 """Switch to a git branch
364 if not branch_exists(new_branch
):
365 raise GitException
, 'Branch "%s" does not exist' % new_branch
367 tree_id
= rev_parse(os
.path
.join('refs', 'heads', new_branch
)
369 if tree_id
!= get_head():
371 if __run('git-read-tree -u -m', [get_head(), tree_id
]) != 0:
372 raise GitException
, 'git-read-tree failed (local changes maybe?)'
374 set_head_file(new_branch
)
376 if os
.path
.isfile(os
.path
.join(basedir
.get(), 'MERGE_HEAD')):
377 os
.remove(os
.path
.join(basedir
.get(), 'MERGE_HEAD'))
379 def delete_branch(name
):
380 """Delete a git branch
382 if not branch_exists(name
):
383 raise GitException
, 'Branch "%s" does not exist' % name
384 remove_file_and_dirs(os
.path
.join(basedir
.get(), 'refs', 'heads'),
387 def rename_branch(from_name
, to_name
):
388 """Rename a git branch
390 if not branch_exists(from_name
):
391 raise GitException
, 'Branch "%s" does not exist' % from_name
392 if branch_exists(to_name
):
393 raise GitException
, 'Branch "%s" already exists' % to_name
395 if get_head_file() == from_name
:
396 set_head_file(to_name
)
397 rename(os
.path
.join(basedir
.get(), 'refs', 'heads'),
400 reflog_dir
= os
.path
.join(basedir
.get(), 'logs', 'refs', 'heads')
401 if os
.path
.exists(reflog_dir
) \
402 and os
.path
.exists(os
.path
.join(reflog_dir
, from_name
)):
403 rename(reflog_dir
, from_name
, to_name
)
406 """Add the files or recursively add the directory contents
408 # generate the file list
411 if not os
.path
.exists(i
):
412 raise GitException
, 'Unknown file or directory: %s' % i
415 # recursive search. We only add files
416 for root
, dirs
, local_files
in os
.walk(i
):
417 for name
in [os
.path
.join(root
, f
) for f
in local_files
]:
418 if os
.path
.isfile(name
):
419 files
.append(os
.path
.normpath(name
))
420 elif os
.path
.isfile(i
):
421 files
.append(os
.path
.normpath(i
))
423 raise GitException
, '%s is not a file or directory' % i
426 if __run('git-update-index --add --', files
):
427 raise GitException
, 'Unable to add file'
429 def rm(files
, force
= False):
430 """Remove a file from the repository
434 if os
.path
.exists(f
):
435 raise GitException
, '%s exists. Remove it first' %f
437 __run('git-update-index --remove --', files
)
440 __run('git-update-index --force-remove --', files
)
448 """Return the user information.
452 name
=config
.get('user.name')
453 email
=config
.get('user.email')
455 __user
= Person(name
, email
)
457 raise GitException
, 'unknown user details'
461 """Return the author information.
466 # the environment variables take priority over config
468 date
= os
.environ
['GIT_AUTHOR_DATE']
471 __author
= Person(os
.environ
['GIT_AUTHOR_NAME'],
472 os
.environ
['GIT_AUTHOR_EMAIL'],
479 """Return the author information.
484 # the environment variables take priority over config
486 date
= os
.environ
['GIT_COMMITTER_DATE']
489 __committer
= Person(os
.environ
['GIT_COMMITTER_NAME'],
490 os
.environ
['GIT_COMMITTER_EMAIL'],
496 def update_cache(files
= None, force
= False):
497 """Update the cache information for the given files
502 cache_files
= __tree_status(files
, verbose
= False)
504 # everything is up-to-date
505 if len(cache_files
) == 0:
508 # check for unresolved conflicts
509 if not force
and [x
for x
in cache_files
510 if x
[0] not in ['M', 'N', 'A', 'D']]:
511 raise GitException
, 'Updating cache failed: unresolved conflicts'
514 add_files
= [x
[1] for x
in cache_files
if x
[0] in ['N', 'A']]
515 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['D']]
516 m_files
= [x
[1] for x
in cache_files
if x
[0] in ['M']]
518 if add_files
and __run('git-update-index --add --', add_files
) != 0:
519 raise GitException
, 'Failed git-update-index --add'
520 if rm_files
and __run('git-update-index --force-remove --', rm_files
) != 0:
521 raise GitException
, 'Failed git-update-index --rm'
522 if m_files
and __run('git-update-index --', m_files
) != 0:
523 raise GitException
, 'Failed git-update-index'
527 def commit(message
, files
= None, parents
= None, allowempty
= False,
528 cache_update
= True, tree_id
= None,
529 author_name
= None, author_email
= None, author_date
= None,
530 committer_name
= None, committer_email
= None):
531 """Commit the current tree to repository
538 # Get the tree status
539 if cache_update
and parents
!= []:
540 changes
= update_cache(files
)
541 if not changes
and not allowempty
:
542 raise GitException
, 'No changes to commit'
544 # get the commit message
547 elif message
[-1:] != '\n':
551 # write the index to repository
553 tree_id
= _output_one_line('git-write-tree')
560 cmd
+= 'GIT_AUTHOR_NAME="%s" ' % author_name
562 cmd
+= 'GIT_AUTHOR_EMAIL="%s" ' % author_email
564 cmd
+= 'GIT_AUTHOR_DATE="%s" ' % author_date
566 cmd
+= 'GIT_COMMITTER_NAME="%s" ' % committer_name
568 cmd
+= 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
569 cmd
+= 'git-commit-tree %s' % tree_id
575 commit_id
= _output_one_line(cmd
, message
)
577 __set_head(commit_id
)
581 def apply_diff(rev1
, rev2
, check_index
= True, files
= None):
582 """Apply the diff between rev1 and rev2 onto the current
583 index. This function doesn't need to raise an exception since it
584 is only used for fast-pushing a patch. If this operation fails,
585 the pushing would fall back to the three-way merge.
588 index_opt
= '--index'
595 diff_str
= diff(files
, rev1
, rev2
)
598 _input_str('git-apply %s' % index_opt
, diff_str
)
604 def merge(base
, head1
, head2
, recursive
= False):
605 """Perform a 3-way merge between base, head1 and head2 into the
611 # this operation tracks renames but it is slower (used in
612 # general when pushing or picking patches)
614 # use _output() to mask the verbose prints of the tool
615 _output('git-merge-recursive %s -- %s %s' %
(base
, head1
, head2
))
619 # the fast case where we don't track renames (used when the
620 # distance between base and heads is small, i.e. folding or
621 # synchronising patches)
622 if __run('git-read-tree -u -m --aggressive',
623 [base
, head1
, head2
]) != 0:
624 raise GitException
, 'git-read-tree failed (local changes maybe?)'
626 # check the index for unmerged entries
628 stages_re
= re
.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re
.S
)
630 for line
in _output('git-ls-files --unmerged --stage -z').split('\0'):
634 mode
, hash, stage
, path
= stages_re
.findall(line
)[0]
636 if not path
in files
:
638 files
[path
]['1'] = ('', '')
639 files
[path
]['2'] = ('', '')
640 files
[path
]['3'] = ('', '')
642 files
[path
][stage
] = (mode
, hash)
644 # merge the unmerged files
647 # remove additional files that might be generated for some
648 # newer versions of GIT
649 for suffix
in [base
, head1
, head2
]:
652 fname
= path
+ '~' + suffix
653 if os
.path
.exists(fname
):
657 if gitmergeonefile
.merge(stages
['1'][1], stages
['2'][1],
658 stages
['3'][1], path
, stages
['1'][0],
659 stages
['2'][0], stages
['3'][0]) != 0:
663 raise GitException
, 'GIT index merging failed (possible conflicts)'
665 def status(files
= None, modified
= False, new
= False, deleted
= False,
666 conflict
= False, unknown
= False, noexclude
= False):
667 """Show the tree status
672 cache_files
= __tree_status(files
, unknown
= True, noexclude
= noexclude
)
673 all
= not (modified
or new
or deleted
or conflict
or unknown
)
688 cache_files
= [x
for x
in cache_files
if x
[0] in filestat
]
690 for fs
in cache_files
:
691 if files
and not fs
[1] in files
:
694 print '%s %s' %
(fs
[0], fs
[1])
698 def diff(files
= None, rev1
= 'HEAD', rev2
= None, out_fd
= None):
699 """Show the diff between rev1 and rev2
705 diff_str
= _output(['git-diff-tree', '-p', rev1
, rev2
, '--'] + files
)
709 diff_str
= _output(['git-diff-index', '-p', '-R', rev2
, '--'] + files
)
711 diff_str
= _output(['git-diff-index', '-p', rev1
, '--'] + files
)
716 out_fd
.write(diff_str
)
720 def diffstat(files
= None, rev1
= 'HEAD', rev2
= None):
721 """Return the diffstat between rev1 and rev2
726 p
=popen2
.Popen3('git-apply --stat')
727 diff(files
, rev1
, rev2
, p
.tochild
)
729 diff_str
= p
.fromchild
.read().rstrip()
731 raise GitException
, 'git.diffstat failed'
734 def files(rev1
, rev2
):
735 """Return the files modified between rev1 and rev2
739 for line
in _output_lines('git-diff-tree -r %s %s' %
(rev1
, rev2
)):
740 result
+= '%s %s\n' %
tuple(line
.rstrip().split(' ',4)[-1].split('\t',1))
742 return result
.rstrip()
744 def barefiles(rev1
, rev2
):
745 """Return the files modified between rev1 and rev2, without status info
749 for line
in _output_lines('git-diff-tree -r %s %s' %
(rev1
, rev2
)):
750 result
+= '%s\n' % line
.rstrip().split(' ',4)[-1].split('\t',1)[-1]
752 return result
.rstrip()
754 def pretty_commit(commit_id
= 'HEAD'):
755 """Return a given commit (log + diff)
757 return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
760 def checkout(files
= None, tree_id
= None, force
= False):
761 """Check out the given or all files
766 if tree_id
and __run('git-read-tree --reset', [tree_id
]) != 0:
767 raise GitException
, 'Failed git-read-tree --reset %s' % tree_id
769 checkout_cmd
= 'git-checkout-index -q -u'
771 checkout_cmd
+= ' -f'
773 checkout_cmd
+= ' -a'
775 checkout_cmd
+= ' --'
777 if __run(checkout_cmd
, files
) != 0:
778 raise GitException
, 'Failed git-checkout-index'
780 def switch(tree_id
, keep
= False):
781 """Switch the tree to the given id
785 if __run('git-read-tree -u -m', [get_head(), tree_id
]) != 0:
786 raise GitException
, 'git-read-tree failed (local changes maybe?)'
790 def reset(files
= None, tree_id
= None, check_out
= True):
791 """Revert the tree changes relative to the given tree_id. It removes
798 cache_files
= __tree_status(files
, tree_id
)
799 # files which were added but need to be removed
800 rm_files
= [x
[1] for x
in cache_files
if x
[0] in ['A']]
802 checkout(files
, tree_id
, True)
803 # checkout doesn't remove files
804 map(os
.remove
, rm_files
)
806 # if the reset refers to the whole tree, switch the HEAD as well
810 def pull(repository
= 'origin', refspec
= None):
811 """Pull changes from the remote repository. Uses 'git-fetch'
812 and moves the stack base.
821 command
= config
.get('stgit.pullcmd')
822 if __run(command
, args
) != 0:
823 raise GitException
, 'Failed "%s %s"' %
(command
, repository
)
825 if (config
.get('stgit.pull-does-rebase')):
827 reset(tree_id
= rev_parse(repository
))
830 """Repack all objects into a single pack
832 __run('git-repack -a -d -f')
834 def apply_patch(filename
= None, diff
= None, base
= None,
836 """Apply a patch onto the current or given index. There must not
837 be any local changes in the tree, otherwise the command fails
849 orig_head
= get_head()
855 _input_str('git-apply --index', diff
)
860 # write the failed diff to a file
861 f
= file('.stgit-failed.patch', 'w+')
864 print >> sys
.stderr
, 'Diff written to the .stgit-failed.patch file'
869 top
= commit(message
= 'temporary commit used for applying a patch',
872 merge(base
, orig_head
, top
)
874 def clone(repository
, local_dir
):
875 """Clone a remote repository. At the moment, just use the
878 if __run('git-clone', [repository
, local_dir
]) != 0:
879 raise GitException
, 'Failed "git-clone %s %s"' \
880 %
(repository
, local_dir
)
882 def modifying_revs(files
, base_rev
):
883 """Return the revisions from the list modifying the given files
885 cmd
= ['git-rev-list', '%s..' % base_rev
, '--']
886 revs
= [line
.strip() for line
in _output_lines(cmd
+ files
)]
891 def refspec_localpart(refspec
):
892 m
= re
.match('^[^:]*:([^:]*)$', refspec
)
896 raise GitException
, 'Cannot parse refspec "%s"' % line
898 def refspec_remotepart(refspec
):
899 m
= re
.match('^([^:]*):[^:]*$', refspec
)
903 raise GitException
, 'Cannot parse refspec "%s"' % line
906 def __remotes_from_config():
907 return config
.sections_matching(r
'remote\.(.*)\.url')
909 def __remotes_from_dir(dir):
910 return os
.listdir(os
.path
.join(basedir
.get(), dir))
913 """Return the list of remotes in the repository
916 return set(__remotes_from_config()) | \
917 set(__remotes_from_dir('remotes')) | \
918 set(__remotes_from_dir('branches'))
920 def remotes_local_branches(remote
):
921 """Returns the list of local branches fetched from given remote
925 if remote
in __remotes_from_config():
926 for line
in config
.getall('remote.%s.fetch' % remote
):
927 branches
.append(refspec_localpart(line
))
928 elif remote
in __remotes_from_dir('remotes'):
929 stream
= open(os
.path
.join(basedir
.get(), 'remotes', remote
), 'r')
931 # Only consider Pull lines
932 m
= re
.match('^Pull: (.*)\n$', line
)
933 branches
.append(refspec_localpart(m
.group(1)))
935 elif remote
in __remotes_from_dir('branches'):
936 # old-style branches only declare one branch
937 branches
.append('refs/heads/'+remote
);
939 raise GitException
, 'Unknown remote "%s"' % remote
943 def identify_remote(branchname
):
944 """Return the name for the remote to pull the given branchname
945 from, or None if we believe it is a local branch.
948 for remote
in remotes_list():
949 if branchname
in remotes_local_branches(remote
):
952 # FIXME: in the case of local branch we should maybe set remote to
953 # "." but are we even sure it is the only case left ?
955 # if we get here we've found nothing