Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
-import sys, os, os.path, re
-from optparse import OptionParser, make_option
-
+import sys, os, os.path, re, email.Utils
from stgit.exception import *
from stgit.utils import *
from stgit.out import *
from stgit import stack, git, basedir
from stgit.config import config, file_extensions
from stgit.lib import stack as libstack
+from stgit.lib import git as libgit
+from stgit.lib import log
# Command exception class
class CmdException(StgException):
pass
# Utility functions
-class RevParseException(StgException):
- """Revision spec parse error."""
- pass
-
def parse_rev(rev):
- """Parse a revision specification into its
- patchname@branchname//patch_id parts. If no branch name has a slash
- in it, also accept / instead of //."""
- if '/' in ''.join(git.get_heads()):
- # We have branch names with / in them.
- branch_chars = r'[^@]'
- patch_id_mark = r'//'
- else:
- # No / in branch names.
- branch_chars = r'[^@/]'
- patch_id_mark = r'(/|//)'
- patch_re = r'(?P<patch>[^@/]+)'
- branch_re = r'@(?P<branch>%s+)' % branch_chars
- patch_id_re = r'%s(?P<patch_id>[a-z.]*)' % patch_id_mark
-
- # Try //patch_id.
- m = re.match(r'^%s$' % patch_id_re, rev)
- if m:
- return None, None, m.group('patch_id')
-
- # Try path[@branch]//patch_id.
- m = re.match(r'^%s(%s)?%s$' % (patch_re, branch_re, patch_id_re), rev)
- if m:
- return m.group('patch'), m.group('branch'), m.group('patch_id')
-
- # Try patch[@branch].
- m = re.match(r'^%s(%s)?$' % (patch_re, branch_re), rev)
- if m:
- return m.group('patch'), m.group('branch'), None
-
- # No, we can't parse that.
- raise RevParseException
+ """Parse a revision specification into its branch:patch parts.
+ """
+ try:
+ branch, patch = rev.split(':', 1)
+ except ValueError:
+ branch = None
+ patch = rev
+
+ return (branch, patch)
def git_id(crt_series, rev):
"""Return the GIT id
"""
- if not rev:
- return None
-
- # try a GIT revision first
+ # TODO: remove this function once all the occurrences were converted
+ # to git_commit()
+ repository = libstack.Repository.default()
+ return git_commit(rev, repository, crt_series.get_name()).sha1
+
+def get_public_ref(branch_name):
+ """Return the public ref of the branch."""
+ public_ref = config.get('branch.%s.public' % branch_name)
+ if not public_ref:
+ public_ref = 'refs/heads/%s.public' % branch_name
+ return public_ref
+
+def git_commit(name, repository, branch_name = None):
+ """Return the a Commit object if 'name' is a patch name or Git commit.
+ The patch names allowed are in the form '<branch>:<patch>' and can
+ be followed by standard symbols used by git rev-parse. If <patch>
+ is '{base}', it represents the bottom of the stack. If <patch> is
+ {public}, it represents the public branch corresponding to the stack as
+ described in the 'publish' command.
+ """
+ # Try a [branch:]patch name first
+ branch, patch = parse_rev(name)
+ if not branch:
+ branch = branch_name or repository.current_branch_name
+
+ # The stack base
+ if patch.startswith('{base}'):
+ base_id = repository.get_stack(branch).base.sha1
+ return repository.rev_parse(base_id +
+ strip_prefix('{base}', patch))
+ elif patch.startswith('{public}'):
+ public_ref = get_public_ref(branch)
+ return repository.rev_parse(public_ref +
+ strip_prefix('{public}', patch),
+ discard_stderr = True)
+
+ # Other combination of branch and patch
try:
- return git.rev_parse(rev + '^{commit}')
- except git.GitException:
+ return repository.rev_parse('patches/%s/%s' % (branch, patch),
+ discard_stderr = True)
+ except libgit.RepositoryException:
pass
- # try an StGIT patch name
+ # Try a Git commit
try:
- patch, branch, patch_id = parse_rev(rev)
- if branch == None:
- series = crt_series
- else:
- series = stack.Series(branch)
- if patch == None:
- patch = series.get_current()
- if not patch:
- raise CmdException, 'No patches applied'
- if patch in series.get_applied() or patch in series.get_unapplied() or \
- patch in series.get_hidden():
- if patch_id in ['top', '', None]:
- return series.get_patch(patch).get_top()
- elif patch_id == 'bottom':
- return series.get_patch(patch).get_bottom()
- elif patch_id == 'top.old':
- return series.get_patch(patch).get_old_top()
- elif patch_id == 'bottom.old':
- return series.get_patch(patch).get_old_bottom()
- elif patch_id == 'log':
- return series.get_patch(patch).get_log()
- if patch == 'base' and patch_id == None:
- return series.get_base()
- except RevParseException:
- pass
- except stack.StackException:
- pass
-
- raise CmdException, 'Unknown patch or revision: %s' % rev
+ return repository.rev_parse(name, discard_stderr = True)
+ except libgit.RepositoryException:
+ raise CmdException('%s: Unknown patch or revision name' % name)
+
+def color_diff_flags():
+ """Return the git flags for coloured diff output if the configuration and
+ stdout allows."""
+ stdout_is_tty = (sys.stdout.isatty() and 'true') or 'false'
+ if config.get_colorbool('color.diff', stdout_is_tty) == 'true':
+ return ['--color']
+ else:
+ return []
def check_local_changes():
if git.local_changes():
def check_conflicts():
if git.get_conflicts():
- raise CmdException('Unsolved conflicts. Please resolve them first'
- ' or revert the changes with "status --reset"')
+ raise CmdException('Unsolved conflicts. Please fix the conflicts'
+ ' then use "git add --update <files>" or revert the'
+ ' changes with "status --reset".')
def print_crt_patch(crt_series, branch = None):
if not branch:
a list. The names can be individual patches and/or in the
patch1..patch2 format.
"""
+ # in case it receives a tuple
+ patch_list = list(patch_list)
patches = []
for name in patch_args:
return patches
def name_email(address):
- p = parse_name_email(address)
- if p:
+ p = email.Utils.parseaddr(address)
+ if p[1]:
return p
else:
raise CmdException('Incorrect "name <email>"/"email (name)" string: %s'
else:
raise CmdException('Incorrect "name <email> date" string: %s' % address)
-def address_or_alias(addr_str):
- """Return the address if it contains an e-mail address or look up
+def address_or_alias(addr_pair):
+ """Return a name-email tuple the e-mail address is valid or look up
the aliases in the config files.
"""
- def __address_or_alias(addr):
- if not addr:
- return None
- if addr.find('@') >= 0:
- # it's an e-mail address
- return addr
- alias = config.get('mail.alias.'+addr)
- if alias:
- # it's an alias
- return alias
- raise CmdException, 'unknown e-mail alias: %s' % addr
-
- addr_list = [__address_or_alias(addr.strip())
- for addr in addr_str.split(',')]
- return ', '.join([addr for addr in addr_list if addr])
+ addr = addr_pair[1]
+ if '@' in addr:
+ # it's an e-mail address
+ return addr_pair
+ alias = config.get('mail.alias.' + addr)
+ if alias:
+ # it's an alias
+ return name_email(alias)
+ raise CmdException, 'unknown e-mail alias: %s' % addr
def prepare_rebase(crt_series):
# pop all patches
# it might be that we use a custom rebase command with its own
# target type
tree_id = target
- if tree_id == git.get_head():
- out.info('Already at "%s", no need for rebasing.' % target)
- return
if target:
out.start('Rebasing to "%s"' % target)
else:
#
def __end_descr(line):
return re.match('---\s*$', line) or re.match('diff -', line) or \
- re.match('Index: ', line)
+ re.match('Index: ', line) or re.match('--- \w', line)
def __split_descr_diff(string):
"""Return the description and the diff from the given string
lasthdr = 0
end = len(descr_lines)
+ descr_strip = 0
# Parse the patch header
for pos in range(0, end):
if subject:
break
# get the subject
- subject = descr_lines[pos]
+ subject = descr_lines[pos][descr_strip:]
+ if re.match('commit [\da-f]{40}$', subject):
+ # 'git show' output, look for the real subject
+ subject = ''
+ descr_strip = 4
lasthdr = pos + 1
# get the body
if lasthdr < end:
- body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
+ body = '\n' + '\n'.join(l[descr_strip:] for l in descr_lines[lasthdr:])
return (subject + body, authname, authemail, authdate)
authname = authemail = None
# '\n\t' can be found on multi-line headers
- descr = __decode_header(msg['subject']).replace('\n\t', ' ')
+ descr = __decode_header(msg['subject'])
+ descr = re.sub('\n[ \t]*', ' ', descr)
authdate = msg['date']
# remove the '[*PATCH*]' expression in the subject
# the rest of the message
msg_text = ''
for part in msg.walk():
- if part.get_content_type() == 'text/plain':
+ if part.get_content_type() in ['text/plain',
+ 'application/octet-stream']:
msg_text += part.get_payload(decode = True)
rem_descr, diff = __split_descr_diff(msg_text)
return (descr, authname, authemail, authdate, diff)
-def parse_patch(text):
+def parse_patch(text, contains_diff):
"""Parse the input text and return (description, authname,
authemail, authdate, diff)
"""
- descr, diff = __split_descr_diff(text)
- descr, authname, authemail, authdate = __parse_description(descr)
+ if contains_diff:
+ (text, diff) = __split_descr_diff(text)
+ else:
+ diff = None
+ (descr, authname, authemail, authdate) = __parse_description(text)
# we don't yet have an agreed place for the creation date.
# Just return None
return getattr(self, n)
return property(new_f)
+def update_commit_data(cd, options):
+ """Return a new CommitData object updated according to the command line
+ options."""
+ # Set the commit message from commandline.
+ if options.message != None:
+ cd = cd.set_message(options.message)
+
+ # Modify author data.
+ cd = cd.set_author(options.author(cd.author))
+
+ # Add Signed-off-by: or similar.
+ if options.sign_str != None:
+ sign_str = options.sign_str
+ else:
+ sign_str = config.get("stgit.autosign")
+ if sign_str != None:
+ cd = cd.set_message(
+ add_sign_line(cd.message, sign_str,
+ cd.committer.name, cd.committer.email))
+
+ # Let user edit the commit message manually, unless
+ # --save-template or --message was specified.
+ if not getattr(options, 'save_template', None) and not options.message:
+ cd = cd.set_message(edit_string(cd.message, '.stgit-new.txt'))
+
+ return cd
+
class DirectoryException(StgException):
pass
class _Directory(object):
- def __init__(self, needs_current_series = True):
+ def __init__(self, needs_current_series = True, log = True):
self.needs_current_series = needs_current_series
+ self.log = log
@readonly_constant_property
def git_dir(self):
try:
).output_one_line()]
def cd_to_topdir(self):
os.chdir(self.__topdir_path)
+ def write_log(self, msg):
+ if self.log:
+ log.compat_log_entry(msg)
class DirectoryAnywhere(_Directory):
def setup(self):
class DirectoryHasRepository(_Directory):
def setup(self):
self.git_dir # might throw an exception
+ log.compat_log_external_mods()
class DirectoryInWorktree(DirectoryHasRepository):
def setup(self):
"""For commands that use the new infrastructure in stgit.lib.*."""
def __init__(self):
self.needs_current_series = False
+ self.log = False # stgit.lib.transaction handles logging
def setup(self):
# This will throw an exception if we don't have a repository.
self.repository = libstack.Repository.default()