from stgit import basedir
from stgit.utils import *
+from stgit.config import config
+from sets import Set
# git exception class
class GitException(Exception):
#
# Classes
#
+
+class Person:
+ """An author, committer, etc."""
+ def __init__(self, name = None, email = None, date = '',
+ desc = None):
+ self.name = self.email = self.date = None
+ if name or email or date:
+ assert not desc
+ self.name = name
+ self.email = email
+ self.date = date
+ elif desc:
+ assert not (name or email or date)
+ def parse_desc(s):
+ m = re.match(r'^(.+)<(.+)>(.*)$', s)
+ assert m
+ return [x.strip() or None for x in m.groups()]
+ self.name, self.email, self.date = parse_desc(desc)
+ def set_name(self, val):
+ if val:
+ self.name = val
+ def set_email(self, val):
+ if val:
+ self.email = val
+ def set_date(self, val):
+ if val:
+ self.date = val
+ def __str__(self):
+ if self.name and self.email:
+ return '%s <%s>' % (self.name, self.email)
+ else:
+ raise GitException, 'not enough identity data'
+
class Commit:
"""Handle the commit objects
"""
def get_log(self):
return self.__log
+ def __str__(self):
+ return self.get_id_hash()
+
# dictionary of Commit objects, used to avoid multiple calls to git
__commits = dict()
return 0
def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
- noexclude = True):
+ noexclude = True, verbose = False):
"""Returns a list of pairs - [status, filename]
"""
+ if verbose and sys.stdout.isatty():
+ print 'Checking for changes in the working directory...',
+ sys.stdout.flush()
+
refresh_index()
if not files:
if fs[1] not in conflicts:
cache_files.append(fs)
+ if verbose and sys.stdout.isatty():
+ print 'done'
+
return cache_files
-def local_changes():
+def local_changes(verbose = True):
"""Return true if there are local changes in the tree
"""
- return len(__tree_status()) != 0
+ return len(__tree_status(verbose = verbose)) != 0
# HEAD value cached
__head = None
rename(os.path.join(basedir.get(), 'refs', 'heads'),
from_name, to_name)
+ reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
+ if os.path.exists(reflog_dir) \
+ and os.path.exists(os.path.join(reflog_dir, from_name)):
+ rename(reflog_dir, from_name, to_name)
+
def add(names):
"""Add the files or recursively add the directory contents
"""
if files:
__run('git-update-index --force-remove --', files)
+# Persons caching
+__user = None
+__author = None
+__committer = None
+
+def user():
+ """Return the user information.
+ """
+ global __user
+ if not __user:
+ name=config.get('user.name')
+ email=config.get('user.email')
+ __user = Person(name, email)
+ return __user;
+
+def author():
+ """Return the author information.
+ """
+ global __author
+ if not __author:
+ try:
+ # the environment variables take priority over config
+ try:
+ date = os.environ['GIT_AUTHOR_DATE']
+ except KeyError:
+ date = ''
+ __author = Person(os.environ['GIT_AUTHOR_NAME'],
+ os.environ['GIT_AUTHOR_EMAIL'],
+ date)
+ except KeyError:
+ __author = user()
+ return __author
+
+def committer():
+ """Return the author information.
+ """
+ global __committer
+ if not __committer:
+ try:
+ # the environment variables take priority over config
+ try:
+ date = os.environ['GIT_COMMITTER_DATE']
+ except KeyError:
+ date = ''
+ __committer = Person(os.environ['GIT_COMMITTER_NAME'],
+ os.environ['GIT_COMMITTER_EMAIL'],
+ date)
+ except KeyError:
+ __committer = user()
+ return __committer
+
def update_cache(files = None, force = False):
"""Update the cache information for the given files
"""
if not files:
files = []
- cache_files = __tree_status(files)
+ cache_files = __tree_status(files, verbose = False)
# everything is up-to-date
if len(cache_files) == 0:
return True
-def merge(base, head1, head2):
+def merge(base, head1, head2, recursive = False):
"""Perform a 3-way merge between base, head1 and head2 into the
local tree
"""
refresh_index()
- if __run('git-read-tree -u -m --aggressive', [base, head1, head2]) != 0:
- raise GitException, 'git-read-tree failed (local changes maybe?)'
+
+ if recursive:
+ # this operation tracks renames but it is slower (used in
+ # general when pushing or picking patches)
+ try:
+ # use _output() to mask the verbose prints of the tool
+ _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
+ except GitException:
+ pass
+ else:
+ # the fast case where we don't track renames (used when the
+ # distance between base and heads is small, i.e. folding or
+ # synchronising patches)
+ if __run('git-read-tree -u -m --aggressive',
+ [base, head1, head2]) != 0:
+ raise GitException, 'git-read-tree failed (local changes maybe?)'
# check the index for unmerged entries
files = {}
# merge the unmerged files
errors = False
for path in files:
+ # remove additional files that might be generated for some
+ # newer versions of GIT
+ for suffix in [base, head1, head2]:
+ if not suffix:
+ continue
+ fname = path + '~' + suffix
+ if os.path.exists(fname):
+ os.remove(fname)
+
stages = files[path]
if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
stages['3'][1], path, stages['1'][0],
cache_files = [x for x in cache_files if x[0] in filestat]
for fs in cache_files:
+ if files and not fs[1] in files:
+ continue
if all:
print '%s %s' % (fs[0], fs[1])
else:
if not files:
__set_head(tree_id)
-def pull(repository = 'origin', refspec = None):
- """Pull changes from the remote repository. At the moment, just
- use the 'git-pull' command
+def fetch(repository = 'origin', refspec = None):
+ """Fetches changes from the remote repository, using 'git-fetch'
+ by default.
"""
- # 'git-pull' updates the HEAD
+ # we update the HEAD
__clear_head_cache()
args = [repository]
if refspec:
args.append(refspec)
- if __run('git-pull', args) != 0:
- raise GitException, 'Failed "git-pull %s"' % repository
+ command = config.get('stgit.pullcmd')
+ if __run(command, args) != 0:
+ raise GitException, 'Failed "%s %s"' % (command, repository)
+
+def repack():
+ """Repack all objects into a single pack
+ """
+ __run('git-repack -a -d -f')
-def apply_patch(filename = None, base = None):
+def apply_patch(filename = None, diff = None, base = None,
+ fail_dump = True):
"""Apply a patch onto the current or given index. There must not
be any local changes in the tree, otherwise the command fails
"""
- def __apply_patch():
+ if diff is None:
if filename:
- return __run('git-apply --index', [filename]) == 0
+ f = file(filename)
else:
- try:
- _input('git-apply --index', sys.stdin)
- except GitException:
- return False
- return True
+ f = sys.stdin
+ diff = f.read()
+ if filename:
+ f.close()
if base:
orig_head = get_head()
switch(base)
else:
- refresh_index() # needed since __apply_patch() doesn't do it
+ refresh_index()
- if not __apply_patch():
+ try:
+ _input_str('git-apply --index', diff)
+ except GitException:
if base:
switch(orig_head)
- raise GitException, 'Patch does not apply cleanly'
- elif base:
+ if fail_dump:
+ # write the failed diff to a file
+ f = file('.stgit-failed.patch', 'w+')
+ f.write(diff)
+ f.close()
+ print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
+
+ raise
+
+ if base:
top = commit(message = 'temporary commit used for applying a patch',
parents = [base])
switch(orig_head)
revs = [line.strip() for line in _output_lines(cmd + files)]
return revs
+
+
+def refspec_localpart(refspec):
+ m = re.match('^[^:]*:([^:]*)$', refspec)
+ if m:
+ return m.group(1)
+ else:
+ raise GitException, 'Cannot parse refspec "%s"' % line
+
+def refspec_remotepart(refspec):
+ m = re.match('^([^:]*):[^:]*$', refspec)
+ if m:
+ return m.group(1)
+ else:
+ raise GitException, 'Cannot parse refspec "%s"' % line
+
+
+def __remotes_from_config():
+ return config.sections_matching(r'remote\.(.*)\.url')
+
+def __remotes_from_dir(dir):
+ d = os.path.join(basedir.get(), dir)
+ if os.path.exists(d):
+ return os.listdir(d)
+ else:
+ return None
+
+def remotes_list():
+ """Return the list of remotes in the repository
+ """
+
+ return Set(__remotes_from_config()) | \
+ Set(__remotes_from_dir('remotes')) | \
+ Set(__remotes_from_dir('branches'))
+
+def remotes_local_branches(remote):
+ """Returns the list of local branches fetched from given remote
+ """
+
+ branches = []
+ if remote in __remotes_from_config():
+ for line in config.getall('remote.%s.fetch' % remote):
+ branches.append(refspec_localpart(line))
+ elif remote in __remotes_from_dir('remotes'):
+ stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
+ for line in stream:
+ # Only consider Pull lines
+ m = re.match('^Pull: (.*)\n$', line)
+ if m:
+ branches.append(refspec_localpart(m.group(1)))
+ stream.close()
+ elif remote in __remotes_from_dir('branches'):
+ # old-style branches only declare one branch
+ branches.append('refs/heads/'+remote);
+ else:
+ raise GitException, 'Unknown remote "%s"' % remote
+
+ return branches
+
+def identify_remote(branchname):
+ """Return the name for the remote to pull the given branchname
+ from, or None if we believe it is a local branch.
+ """
+
+ for remote in remotes_list():
+ if branchname in remotes_local_branches(remote):
+ return remote
+
+ # if we get here we've found nothing, the branch is a local one
+ return None
+
+def fetch_head():
+ """Return the git id for the tip of the parent branch as left by
+ 'git fetch'.
+ """
+
+ fetch_head=None
+ stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
+ for line in stream:
+ # Only consider lines not tagged not-for-merge
+ m = re.match('^([^\t]*)\t\t', line)
+ if m:
+ if fetch_head:
+ raise GitException, "StGit does not support multiple FETCH_HEAD"
+ else:
+ fetch_head=m.group(1)
+ stream.close()
+
+ # here we are sure to have a single fetch_head
+ return fetch_head