#!/usr/bin/env python3
#
# git-publish - Prepare and store patch revisions as git tags
#
# Copyright 2011 IBM, Corp.
# Copyright 2014-2019 Red Hat, Inc.
#
# Authors:
#   Stefan Hajnoczi <stefanha@gmail.com>
#
# This work is licensed under the MIT License.  Please see the LICENSE file or
# http://opensource.org/licenses/MIT.

from __future__ import print_function, unicode_literals
from io import open
import os
import glob
import sys
import optparse
import re
import tempfile
import shutil
import subprocess
import locale
from email import message_from_file, header

VERSION = '1.6.0'

tag_version_re = re.compile(r'^[a-zA-Z0-9_/\-\.]+-v(\d+)$')

ENCODING = locale.getpreferredencoding()

# As a git alias it is helpful to be a single file script with no external
# dependencies, so these git command-line wrappers are used instead of
# python-git.

class GitSendEmailError(Exception):
    pass

class GitError(Exception):
    pass

class GitHookError(Exception):
    pass

class InspectEmailsError(Exception):
    pass

def to_text(data):
    if isinstance(data, bytes):
        return data.decode(ENCODING)
    return data

def popen_lines(cmd, **kwargs):
    '''Communicate with a Popen object and return a list of lines for stdout and stderr'''
    stdout, stderr = cmd.communicate(**kwargs)
    stdout = stdout.decode(ENCODING).split(os.linesep)[:-1]
    stderr = stderr.decode(ENCODING).split(os.linesep)[:-1]
    return stdout, stderr

def _git_check(*args):
    '''Run a git command and return a list of lines, may raise GitError'''
    cmdstr = 'git ' + ' '.join(('"%s"' % arg if ' ' in arg else arg) for arg in args)
    if VERBOSE:
        print(cmdstr)
    cmd = subprocess.Popen(['git'] + list(args),
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    stdout, stderr = popen_lines(cmd)
    if cmd.returncode != 0:
        raise GitError('ERROR: %s\n%s' % (cmdstr, '\n'.join(stderr)))
    return stdout

def _git(*args):
    '''Run a git command and return a list of lines, ignore errors'''
    try:
        return _git_check(*args)
    except GitError:
        # ignore git command errors
        return []

def _git_with_stderr(*args):
    '''Run a git command and return a list of lines for stdout and stderr'''
    if VERBOSE:
        print('git ' + ' '.join(args))
    cmd = subprocess.Popen(['git'] + list(args),
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    return popen_lines(cmd)

def bool_from_str(s):
    '''Parse a boolean string value like true/false, yes/no, or on/off'''
    return s.lower() in ('true', 'yes', 'on')

def git_get_config(*components):
    '''Get a git-config(1) variable'''
    lines = _git('config', '.'.join(components))
    if len(lines):
        return lines[0]
    return None

def git_get_config_list(*components):
    '''Get a git-config(1) list variable'''
    return _git('config', '--get-all', '.'.join(components))

def git_unset_config(*components):
    _git('config', '--unset-all', '.'.join(components))

def git_set_config(*components):
    '''Set a git-config(1) variable'''
    if len(components) < 2:
        raise TypeError('git_set_config() takes at least 2 arguments (%d given)' % len(components))

    val = components[-1]
    name = '.'.join(components[:-1])

    if isinstance(val, (str, bytes)) or not hasattr(val, '__iter__'):
        _git('config', name, val)
    else:
        git_unset_config(name)
        for v in val:
            _git('config', '--add', name, v)

def git_get_var(name):
    '''Get a git-var(1)'''
    lines = _git('var', name)
    if len(lines):
        return lines[0]
    return None

def git_get_current_branch():
    git_dir = git_get_git_dir()
    rebase_dir = os.path.join(git_dir, 'rebase-merge')
    if os.path.exists(rebase_dir):
        branch_path = os.path.join(rebase_dir, 'head-name')
        prefix = 'refs/heads/'
        branch = open(branch_path).read().strip()
        if branch.startswith(prefix):
            return branch[len(prefix):]
        return branch
    else:
        return _git_check('symbolic-ref', '--short', 'HEAD')[0]

GIT_TOPLEVEL = None
def git_get_toplevel_dir():
    global GIT_TOPLEVEL
    if GIT_TOPLEVEL is None:
        GIT_TOPLEVEL = _git_check('rev-parse', '--show-toplevel')[0]
    return GIT_TOPLEVEL

GIT_DIR = None
def git_get_git_dir():
    global GIT_DIR
    if GIT_DIR is None:
        GIT_DIR = _git('rev-parse', '--git-dir')[0]
    return GIT_DIR

def git_delete_tag(name):
    # Hide stderr when tag does not exist
    _git_with_stderr('tag', '-d', name)

def git_get_tags(pattern=None):
    if pattern:
        return _git('tag', '-l', pattern)
    else:
        return _git('tag')

def git_get_tag_message(tag):
    r = _git('tag', '-l', '--format=%(contents)', tag)
    # --format=%(contents) will print an extra newline if the tag message
    # already ends with a newline, so drop the extra line at the end:
    if r and r[-1] == '':
        r.pop()
    return r

def git_get_remote_url(remote):
    '''Return the URL for a given remote'''
    return _git_check('ls-remote', '--get-url', remote)[0]

def git_request_pull(base, remote, signed_tag):
    return _git_check('request-pull', base, remote, signed_tag)

def git_log(revlist):
    return _git('log', '--no-color', '--oneline', revlist)

def git_tag(name, annotate=None, force=False, sign=False, keyid=None):
    args = ['tag', '--annotate']
    if annotate:
        args += ['--file', annotate]
    else:
        args += ['--message', '']
    if force:
        args += ['--force']
    if sign:
        args += ['--sign']
    if keyid:
        args += ['--local-user', keyid]
    args += [name]
    _git_check(*args)

def git_format_patch(revlist, subject_prefix=None, output_directory=None,
                     numbered=False, cover_letter=False, signoff=False,
                     notes=False, binary=True, headers=[], extra_args=[]):
    args = ['format-patch']
    if subject_prefix:
        args += ['--subject-prefix', subject_prefix]
    if output_directory:
        args += ['--output-directory', output_directory]
    if numbered:
        args += ['--numbered']
    if cover_letter:
        args += ['--cover-letter']
    else:
        args += ['--no-cover-letter']
    if signoff:
        args += ['--signoff']
    if notes:
        args += ['--notes']
    if not binary:
        args += ['--no-binary']

    for header in headers:
        args += ['--add-header', header]

    args += [revlist]
    args += extra_args
    _git_check(*args)

def git_send_email(to_list, cc_list, patches, suppress_cc, in_reply_to, dry_run=False):
    args = ['git', 'send-email']
    for address in to_list:
        args += ['--to', address]
    for address in cc_list:
        args += ['--cc', address]
    if suppress_cc:
        args += ['--suppress-cc', suppress_cc]
    if in_reply_to:
        args += ['--in-reply-to', in_reply_to]
    if dry_run:
        args += ['--dry-run', '--relogin-delay=0']
    else:
        args += ['--quiet']
    args += ['--confirm=never']
    args += patches
    if dry_run:
            return _git_with_stderr(*args[1:])[0]
    else:
        if subprocess.call(args) != 0:
            raise GitSendEmailError

GIT_HOOKDIR = None
def git_get_hook_dir():
    global GIT_HOOKDIR
    if GIT_HOOKDIR is None:
        common_dir = _git('rev-parse', '--git-common-dir')[0]
        if common_dir.startswith("--git-common-dir"):
            common_dir = git_get_git_dir()
        GIT_HOOKDIR = os.path.join(common_dir, 'hooks')
    return GIT_HOOKDIR

def invoke_hook(name, *args):
    '''Run a githooks(5) script'''
    hooks_path = git_get_config("core", "hooksPath") or \
                    os.path.join(git_get_hook_dir())
    hook_path = os.path.join(hooks_path, name)
    if not os.access(hook_path, os.X_OK):
        return
    if subprocess.call((hook_path,) + args, cwd=git_get_toplevel_dir()) != 0:
        raise GitHookError

def git_push(remote, ref, force=False):
    args = ['push']
    if force:
        args += ['-f']
    args += [remote, ref]
    _git_check(*args)

def git_config_with_profile(*args):
    '''Like git-config(1) except with .gitpublish added to the file lookup chain

    Note that only git-config(1) read operations are supported.  Write
    operations are not allowed since we should not modify .gitpublish.'''
    cmd = subprocess.Popen(['git', 'config', '--includes', '--file', '/dev/stdin'] + list(args),
                           stdin=subprocess.PIPE,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)

    # git-config(1) --includes requires absolute paths
    gitpublish = os.path.abspath(os.path.join(git_get_toplevel_dir(), '.gitpublish'))
    if 'GIT_CONFIG' in os.environ:
        gitconfig = os.path.abspath(os.environ['GIT_CONFIG'])
    else:
        gitconfig = os.path.abspath(os.path.join(git_get_git_dir(), 'config'))

    git_config_file = '''
[include]
    path = ~/.gitconfig
    path = %s
    path = %s
''' % (gitpublish,  gitconfig)

    stdout, _ = popen_lines(cmd, input=git_config_file.encode(ENCODING))
    return stdout

def git_cover_letter_info(base, topic, to, cc, in_reply_to, number):
    cl_info = ['Lines starting with \'#\' will be ignored.']
    cl_info += ['']

    cl_info += ['Version number: ' + str(number)]
    cl_info += ['Branches:']
    cl_info += ['         base:  ' + base, '         topic: ' + topic]
    cl_info += ['']

    if to:
        cl_info += ['To: ' + '\n#     '.join(list(to))]
    if cc:
        cl_info += ['Cc: ' + '\n#     '.join(list(cc))]
    if in_reply_to:
        cl_info += ['In-Reply-To: ' + in_reply_to]
    cl_info += ['']

    cl_info += _git('shortlog', base + '..' + topic)
    cl_info += _git('diff', '--stat', base + '..' + topic)

    return ["#" + (l if l == '' else ' ' + l) for l in cl_info]

def check_profile_exists(profile_name):
    '''Return True if the profile exists, False otherwise'''
    lines = git_config_with_profile('--get-regexp', '^gitpublishprofile\\.%s\\.' % profile_name)
    return bool(lines)

def has_profiles():
    '''Return True if any profile exists, False otherwise'''
    lines = git_config_with_profile('--get-regexp', '^gitpublishprofile\\.*\\.')
    return bool(lines)

def get_profile_var(profile_name, var_name):
    '''Get a profile variable'''
    option = '.'.join(['gitpublishprofile', profile_name, var_name])
    lines = git_config_with_profile(option)
    if len(lines):
        return lines[0]
    return None

def get_profile_var_list(profile_name, var_name):
    '''Get a profile list variable'''
    option = '.'.join(['gitpublishprofile', profile_name, var_name])
    return git_config_with_profile('--get-all', option)

def setup():
    '''Add git alias in ~/.gitconfig'''
    path = os.path.abspath(sys.argv[0])
    ret = subprocess.call(['git', 'config', '--global',
                           'alias.publish', '!' + path])
    if ret == 0:
        print('You can now use \'git publish\' like a built-in git command.')

def tag_name(topic, number):
    '''Build a tag name from a topic name and version number'''
    return '%s-v%d' % (topic, number)

def tag_name_staging(topic):
    '''Build a staging tag name from a topic name'''
    return '%s-staging' % topic

def tag_name_pull_request(topic):
    '''Build a pull request tag name from a topic name'''
    return '%s-pull-request' % topic

def get_latest_tag_number(branch):
    '''Find the latest tag number or 0 if no tags exist'''
    number = 0
    for tag in git_get_tags('%s-v[0-9]*' % branch):
        m = tag_version_re.match(tag)
        if not m:
            continue
        n = int(m.group(1))
        if n > number:
            number = n
    return number

def get_latest_tag_message(topic, default_lines):
    '''Find the latest tag message or return a template if no tags exist'''
    msg = git_get_tag_message(tag_name_staging(topic))
    if msg:
        return msg

    number = get_latest_tag_number(topic)
    msg = git_get_tag_message(tag_name(topic, number))
    if msg:
        return msg

    return default_lines

def get_pull_request_message(base, remote, topic):
    # Add a subject line
    message = [topic.replace('_', ' ').replace('-', ' ').capitalize() + ' patches',
               '']
    output = git_request_pull(base, remote, tag_name_pull_request(topic))

    # Chop off diffstat because git-send-email(1) will generate it
    first_separator = True
    for line in output:
        message.append(line)
        if line == '----------------------------------------------------------------':
            if not first_separator:
                break
            first_separator = False

    return message

def get_number_of_commits(base):
    return len(git_log('%s..' % base))

def edit(*filenames):
    cmd = git_get_var('GIT_EDITOR').split(" ")
    cmd.extend(filenames)
    subprocess.call(cmd)

def tag(name, template, annotate=False, force=False, sign=False, keyid=None):
    '''Edit a tag message and create the tag'''
    fd, tmpfile = None, None

    try:
        if annotate:
            fd, tmpfile = tempfile.mkstemp(text=True)
            os.fdopen(fd, 'wb').write(os.linesep.join(template + ['']).encode(ENCODING))
            edit(tmpfile)

        git_tag(name, annotate=tmpfile, force=force, sign=sign, keyid=keyid)
    finally:
        if tmpfile:
            os.unlink(tmpfile)

def menu_select(menu):
    while True:
        for k, v in menu:
            print("[%s] %s" % (k, v))
        a = sys.stdin.readline().strip()
        if a not in [k for (k, v) in menu]:
            print("Unknown command, please retry")
            continue
        return a

def parse_header(hdr):
    r = ''
    for h, c in header.decode_header(hdr):
        if c is None:
            c = 'us-ascii'

        if sys.version_info > (3, 0) and type(h) is str:
            r += h
        else:
            r += h.decode(c)

    if '\n' in r:
        r = " ".join([x.strip() for x in r.splitlines()])
    return r

def edit_email_list(cc_list):
    tmpfile = tempfile.NamedTemporaryFile(mode='wb', suffix='.txt')
    tmpfile.write(os.linesep.join(cc_list).encode(ENCODING))
    tmpfile.flush()
    edit(tmpfile.name)
    r = []
    for line in open(tmpfile.name, "r").readlines():
        r += [x.strip() for x in line.split(",")]
    return r

def git_save_email_lists(topic, to, cc, override_cc):
    # Store --to and --cc for next revision
    git_set_config('branch', topic, 'gitpublishto', to)
    if not override_cc:
        git_set_config('branch', topic, 'gitpublishcc', cc)

def inspect_menu(tmpdir, to_list, cc_list, patches, suppress_cc, in_reply_to,
                 topic, override_cc):
    while True:
        print('Stopping so you can inspect the patch emails:')
        print('  cd %s' % tmpdir)
        print()
        output = git_send_email(to_list, cc_list, patches, suppress_cc,
                                in_reply_to, dry_run=True)
        index = 0
        for f in patches:
            m = message_from_file(open(f))
            print(parse_header(m['subject']))
            # Print relevant 'Adding cc' lines from the git-send-email --dry-run output
            while index < len(output) and len(output[index]):
                if output[index].find('Adding cc') != -1:
                    print('  ' + output[index])
                index += 1
            index += 1
        print()
        print("To:", "\n    ".join(to_list))
        if cc_list:
            print("Cc:", "\n    ".join(cc_list))
        if in_reply_to:
            print("In-Reply-To:", in_reply_to)
        print()
        a = menu_select([
                ('c', 'Edit Cc list in editor (save after edit)'),
                ('t', 'Edit To list in editor (save after edit)'),
                ('e', 'Edit patches in editor'),
                ('s', 'Select patches to send (default: all)'),
                ('p', 'Print final email headers (dry run)'),
                ('a', 'Send all'),
                ('q', 'Cancel (quit)'),
            ])
        if a == 'q':
            raise InspectEmailsError
        elif a == 'c':
            new_cc_list = edit_email_list(cc_list)
            cc_list.clear()
            cc_list.update(new_cc_list)
            git_save_email_lists(topic, to_list, cc_list, override_cc)
        elif a == 't':
            new_to_list = edit_email_list(to_list)
            to_list.clear()
            to_list.update(new_to_list)
            git_save_email_lists(topic, to_list, cc_list, override_cc)
        elif a == 'e':
            edit(*patches)
        elif a == 's':
            listfile = tempfile.NamedTemporaryFile()
            listfile.write("\n".join(patches).encode(ENCODING))
            listfile.flush()
            edit(listfile.name)
            listfile.seek(0)
            patches = [x for x in listfile.read().splitlines() if len(x.strip())]
        elif a == 'p':
            print('\n'.join(output))
        elif a == 'a':
            break
    return patches

def parse_args():

    parser = optparse.OptionParser(version='%%prog %s' % VERSION,
            usage='%prog [options] -- [common format-patch options]',
            description='Prepare and store patch revisions as git tags.',
            epilog='Please report bugs to Stefan Hajnoczi <stefanha@gmail.com>.')
    parser.add_option('--annotate', dest='annotate', action='store_true',
                      default=False, help='review and edit each patch email')
    parser.add_option('-b', '--base', dest='base', default=None,
                      help='branch which this is based off [defaults to master]')
    parser.add_option('--blurb-template', dest='blurb_template', default=None,
                      help='Template for blurb [defaults to *** BLURB HERE ***]')
    parser.add_option('--cc', dest='cc', action='append', default=[],
                      help='specify a Cc: email recipient')
    parser.add_option('--cc-cmd',
                      help='specify a command whose output to add to the cc list')
    parser.add_option('--no-check-url', dest='check_url', action='store_false',
                      help='skip publicly accessible pull request URL check')
    parser.add_option('--check-url', dest='check_url', action='store_true',
                      help='check pull request URLs are publicly accessible')
    parser.add_option('--edit', dest='edit', action='store_true',
                      default=False, help='edit message but do not tag a new version')
    parser.add_option('--no-inspect-emails', dest='inspect_emails',
                      action='store_false',
                      help='no confirmation before sending emails')
    parser.add_option('--inspect-emails', dest='inspect_emails',
                      action='store_true', default=True,
                      help='show confirmation before sending emails')
    parser.add_option('-n', '--number', type='int', dest='number', default=-1,
                      help='version number [auto-generated by default]')
    parser.add_option('--no-message', '--no-cover-letter', dest='message',
                      action='store_false', help='do not add a message')
    parser.add_option('-m', '--message', '--cover-letter', dest='message',
                      action='store_true', help='add a message')
    parser.add_option('--no-cover-info', dest='cover_info',
                      action='store_false', default=True,
                      help='do not append comments information when editing the cover letter')
    parser.add_option('--no-binary', dest='binary',
                      action='store_false', default=True,
                      help='Do not output contents of changes in binary files, instead display a notice that those files changed')
    parser.add_option('--profile', '-p', dest='profile_name', default='default',
                      help='select default settings profile')
    parser.add_option('--pull-request', dest='pull_request', action='store_true',
                      default=False, help='tag and send as a pull request')
    parser.add_option('--sign-pull', dest='sign_pull', action='store_true',
                      help='sign tag when sending pull request')
    parser.add_option('-k', '--keyid', dest='keyid',
                      help='use the given GPG key when signing pull request tag')
    parser.add_option('--no-sign-pull', dest='sign_pull', action='store_false',
                      help='do not sign tag when sending pull request')
    parser.add_option('--subject-prefix', dest='prefix', default=None,
                      help='set the email Subject: header prefix')
    parser.add_option('--clear-subject-prefix', dest='clear_prefix',
                      action='store_true', default=False,
                      help='clear the per-branch subject prefix')
    parser.add_option('--setup', dest='setup', action='store_true', default=False,
                      help='add git alias in ~/.gitconfig')
    parser.add_option('-t', '--topic', dest='topic',
                      help='topic name [defaults to current branch name]')
    parser.add_option('--to', dest='to', action='append', default=[],
                      help='specify a primary email recipient')
    parser.add_option('-s', '--signoff', dest='signoff', action='store_true',
                      default=False,
                      help='add Signed-off-by: <self> to commits when emailing')
    parser.add_option('--notes', dest='notes', action='store_true',
                      default=False,
                      help='Append the notes (see git-notes(1)) for the commit after the three-dash line.')
    parser.add_option('--suppress-cc', dest='suppress_cc',
                      help='override auto-cc when sending email (man git-send-email for details)')
    parser.add_option('-v', '--verbose', dest='verbose',
                      action='store_true', default=False,
                      help='show executed git commands (useful for troubleshooting)')
    parser.add_option('--forget-cc', dest='forget_cc', action='store_true',
                      default=False, help='Forget all previous CC emails')
    parser.add_option('--override-to', dest='override_to', action='store_true',
                      default=False, help='Ignore any profile or saved TO emails')
    parser.add_option('--override-cc', dest='override_cc', action='store_true',
                      default=False, help='Ignore any profile or saved CC emails')
    parser.add_option('--in-reply-to', "-R",
                      help='specify the In-Reply-To: of the cover letter (or the single patch)')
    parser.add_option('--add-header', '-H', action='append', dest='headers',
                      help='specify custom headers to git-send-email')

    return parser.parse_args()

def main():
    global VERBOSE

    options, args = parse_args()
    VERBOSE = options.verbose

    # The --edit option is for editing the cover letter without publishing a
    # new revision.  Therefore it doesn't make sense to combine it with options
    # that create new revisions.
    if options.edit and any((options.annotate, options.number != -1,
                             options.setup, options.to, options.pull_request)):
        print('The --edit option cannot be used together with other options')
        return 1

    # Keep this before any operations that call out to git(1) so that setup
    # works when the current working directory is outside a git repo.
    if options.setup:
        setup()
        return 0

    if not check_profile_exists(options.profile_name):
        if options.profile_name == 'default':
            if has_profiles():
                print('Using defaults when a non-default profile exists. Forgot to pass --profile ?')
        else:
            print('Profile "%s" does not exist, please check .gitpublish or git-config(1) files' % options.profile_name)
            return 1

    current_branch = git_get_current_branch()

    if options.topic:
        topic = options.topic
    else:
        topic = current_branch

    base = options.base
    if not base:
        base = git_get_config('branch', current_branch, 'gitpublishbase')
    if not base:
        base = get_profile_var(options.profile_name, 'base')
    if not base:
        base = git_get_config('git-publish', 'base')
    if not base:
        base = 'master'

    if topic == base:
        print('Please use a topic branch, cannot version the base branch (%s)' % base)
        return 1

    if options.number >= 0:
        number = options.number
    elif options.pull_request:
        number = 1
    else:
        number = get_latest_tag_number(topic) + 1

    to = set([to_text(_) for _ in options.to])
    if not options.edit and not options.override_to:
        to = to.union(git_get_config_list('branch', topic, 'gitpublishto'))
        to = to.union(get_profile_var_list(options.profile_name, 'to'))

    if options.forget_cc:
        git_set_config('branch', topic, 'gitpublishcc', [])

    cc = set([to_text(_) for _ in options.cc])
    if not options.edit and not options.override_cc:
        cc = cc.union(git_get_config_list('branch', topic, 'gitpublishcc'))
        cc = cc.union(get_profile_var_list(options.profile_name, 'cc'))

    cc_cmd = options.cc_cmd
    if not cc_cmd:
        cc_cmd = git_get_config('branch', topic, 'gitpublishcccmd') or \
                 get_profile_var(options.profile_name, 'cccmd')

    blurb_template = options.blurb_template
    if not blurb_template:
        blurb_template = '\n'.join(get_profile_var_list(options.profile_name, 'blurb-template'))
    if not blurb_template:
        blurb_template = "*** BLURB HERE ***"

    headers = options.headers
    if not headers:
        headers = []

    if options.pull_request:
        remote = git_get_config('branch', topic, 'pushRemote')
        if remote is None:
            remote = git_get_config('remote', 'pushDefault')
        if remote is None:
            remote = git_get_config('branch', topic, 'remote')
        if remote is None or remote == '.':
            remote = get_profile_var(options.profile_name, 'remote')
        if remote is None:
            print('''Unable to determine remote repo to push.  Please set git config
branch.%s.pushRemote, branch.%s.remote, remote.pushDefault, or
gitpublishprofile.%s.remote''' % (topic, topic, options.profile_name))
            return 1

        check_url = options.check_url
        if check_url is None:
            check_url_var = get_profile_var(options.profile_name, 'checkUrl')
            if check_url_var is None:
                check_url_var = git_get_config('git-publish', 'checkUrl')
            if check_url_var is not None:
                check_url = bool_from_str(check_url_var)
        if check_url is None:
            check_url = True

        url = git_get_remote_url(remote)
        if check_url and not any(url.startswith(scheme) for scheme in ('git://', 'http://', 'https://')):
            print('''Possible private URL "%s", normally pull requests reference publicly
accessible git://, http://, or https:// URLs.  Are you sure
branch.%s.pushRemote is set appropriately?  (Override with --no-url-check)''' % (url, topic))
            return 1

        sign_pull = options.sign_pull
        if sign_pull is None:
            sign_pull_var = get_profile_var(options.profile_name, 'signPull')
            if sign_pull_var is None:
                sign_pull_var = git_get_config('git-publish', 'signPull')
            if sign_pull_var is not None:
                sign_pull = bool_from_str(sign_pull_var)
        if sign_pull is None:
            sign_pull = True

    profile_message_var = get_profile_var(options.profile_name, 'message')
    if options.message is not None:
        message = options.message
    elif git_get_tag_message(tag_name_staging(topic)):
        # If there is a staged tag message, we definitely want a cover letter
        message = True
    elif profile_message_var is not None:
        message = bool_from_str(profile_message_var)
    elif options.pull_request:
        # Pull requests always get a cover letter by default
        message = True
    else:
        config_cover_letter = git_get_config('format', 'coverLetter')
        if config_cover_letter is None or config_cover_letter.lower() == 'auto':
            # If there are several commits we probably want a cover letter
            message = get_number_of_commits(base) > 1
        else:
            message = bool_from_str(config_cover_letter)

    keyid = options.keyid
    if keyid is None:
        keyid_var = get_profile_var(options.profile_name, 'signingkey')
        if keyid_var is None:
            keyid_var = git_get_config('git-publish', 'signingkey')

    invoke_hook('pre-publish-tag', base)

    cl_info = ['']
    if options.cover_info:
        cl_info += git_cover_letter_info(base, topic, to, cc, options.in_reply_to, number)

    # Tag the tree
    if options.pull_request:
        tag_message = get_latest_tag_message(topic, ['Pull request'])
        tag_message += cl_info
        tag(tag_name_pull_request(topic), tag_message, annotate=message, force=True, sign=sign_pull, keyid=keyid)
        git_push(remote, tag_name_pull_request(topic), force=True)
    else:
        tag_message = get_latest_tag_message(topic, [
            '*** SUBJECT HERE ***',
            '',
            blurb_template])
        tag_message += cl_info
        anno = options.edit or message
        tag(tag_name_staging(topic), tag_message, annotate=anno, force=True)

    if options.clear_prefix:
        git_unset_config('branch', topic, 'gitpublishprefix')

    prefix = options.prefix
    if prefix is not None:
        git_set_config('branch', topic, 'gitpublishprefix', prefix)
    else:
        prefix = git_get_config('branch', topic, 'gitpublishprefix')
    if prefix is None:
        prefix = get_profile_var(options.profile_name, 'prefix')
    if prefix is None:
        if options.pull_request:
            prefix = 'PULL'
        else:
            prefix = git_get_config('format', 'subjectprefix') or 'PATCH'
    if number > 1:
        prefix = '%s v%d' % (prefix, number)

    if to:
        if options.pull_request:
            message = get_pull_request_message(base, remote, topic)
        else:
            message = git_get_tag_message(tag_name_staging(topic))
        suppress_cc = options.suppress_cc
        if suppress_cc is None:
            suppress_cc = get_profile_var(options.profile_name, 'suppresscc')

        if options.signoff:
            signoff = True
        else:
            signoff = get_profile_var(options.profile_name, 'signoff')

        if options.inspect_emails:
            inspect_emails = True
        else:
            inspect_emails = get_profile_var(options.profile_name, 'inspect-emails')

        if options.notes:
            notes = True
        else:
            notes = get_profile_var(options.profile_name, 'notes')

        try:
            tmpdir = tempfile.mkdtemp()
            numbered = get_number_of_commits(base) > 1 or message
            git_format_patch(base + '..',
                             subject_prefix=prefix,
                             output_directory=tmpdir,
                             numbered=numbered,
                             cover_letter=message,
                             signoff=signoff,
                             notes=notes,
                             binary=options.binary,
                             headers=headers,
                             extra_args=args)
            if message:
                cover_letter_path = os.path.join(tmpdir, '0000-cover-letter.patch')
                lines = open(cover_letter_path).readlines()
                lines = [s.replace('*** SUBJECT HERE ***', message[0]) for s in lines]
                blurb = os.linesep.join(message[2:])
                lines = [s.replace('*** BLURB HERE ***', blurb) for s in lines]
                open(cover_letter_path, 'w').writelines(lines)
            patches = sorted(glob.glob(os.path.join(tmpdir, '*')))
            if options.annotate:
                edit(*patches)
            if cc_cmd:
                for x in patches:
                    output = subprocess.check_output(cc_cmd + " " + x,
                                shell=True, cwd=git_get_toplevel_dir()).decode(ENCODING)
                    cc = cc.union(output.splitlines())
            cc.difference_update(to)
            if inspect_emails:
                selected_patches = inspect_menu(tmpdir, to, cc, patches, suppress_cc,
                                                options.in_reply_to, topic,
                                                options.override_cc)
            else:
                selected_patches = patches

            invoke_hook('pre-publish-send-email', tmpdir)

            final_patches = sorted(glob.glob(os.path.join(tmpdir, '*')))
            if final_patches != patches:
                added = set(final_patches).difference(set(patches))
                deleted = set(patches).difference(set(final_patches))
                print("The list of files in %s changed and I don't know what to do" % tmpdir)
                if added:
                    print('Added files: %s' % ' '.join(added))
                if deleted:
                    print('Deleted files: %s' % ' '.join(deleted))
                return 1

            git_send_email(to, cc, selected_patches, suppress_cc, options.in_reply_to)
        except (GitSendEmailError, GitHookError, InspectEmailsError):
            return 1
        except GitError as e:
            print(e)
            return 1
        finally:
            if tmpdir:
                shutil.rmtree(tmpdir)

        git_save_email_lists(topic, to, cc, options.override_cc)

        if not options.pull_request:
            # Publishing is done, stablize the tag now
            _git_check('tag', '-f', tag_name(topic, number), tag_name_staging(topic))
            git_delete_tag(tag_name_staging(topic))

    return 0

if __name__ == '__main__':
    sys.exit(main())
