#!/usr/bin/python
#
# git-publish - Prepare and store patch revisions as git tags
#
# Copyright 2011 IBM, Corp.
# Copyright 2014 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 signal
from email import message_from_file, header

VERSION = '1.4.2'

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

# 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 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('utf-8').split(os.linesep)[:-1]
    stderr = stderr.decode('utf-8').split(os.linesep)[:-1]
    return stdout, stderr

def _git_check(*args):
    '''Run a git command and return a list of lines, may raise GitError'''
    if VERBOSE:
        print('git ' + ' '.join(('"%s"' % arg if ' ' in arg else arg) for arg in args))
    cmd = subprocess.Popen(['git'] + list(args),
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    stdout, stderr = popen_lines(cmd)
    if cmd.returncode != 0:
        raise GitError('\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 hasattr(val, '__iter__'):
        git_unset_config(name)
        for v in val:
            _git('config', '--add', name, v)
    else:
        _git('config', name, val)

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():
    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('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):
    message = []
    # Hide stderr when tag does not exist
    for line in _git_with_stderr('show', '--raw', '--no-color',
                                 '--pretty=medium', tag)[0][4:]:
        if line.startswith('commit '):
            message.pop()
            return message
        message.append(line)
    return None

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):
    args = ['tag', '-a']
    if annotate:
        args += ['-F', annotate]
    else:
        args += ['-m', '']
    if force:
        args += ['-f']
    if sign:
        args += ['-s']
    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):
    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']
    args += [revlist]
    _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']
    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 = %s
    path = %s
    path = ~/.gitconfig
''' % (gitpublish,  gitconfig)

    stdout, _ = popen_lines(cmd, input=git_config_file.encode('utf-8'))
    return stdout

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 get_first_profile():
    '''Return name of the first profile, None if no profile is found'''
    lines = git_config_with_profile('--get-regexp', '^gitpublishprofile\\.*\\.')
    if len(lines):
        return lines[0].split(".")[1]
    return None

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):
    '''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 ['*** SUBJECT HERE ***', '', '*** BLURB HERE ***']

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):
    '''Edit a tag message and create the tag'''
    fd, tmpfile = None, None

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

        git_tag(name, annotate=tmpfile, force=force, sign=sign)
    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):
    try:
        r = ''
        for h, c in header.decode_header(hdr):
            r += unicode(h, c) if c else h
        if '\n' in r:
            r = " ".join([x.strip() for x in r.splitlines()])
        return r
    except:
        sys.stderr.write("Failed to parse email header: %s\n", hdr)
        return hdr

def edit_email_list(cc_list):
    tmpfile = tempfile.NamedTemporaryFile(mode='wb', suffix='.txt')
    tmpfile.write(os.linesep.join(cc_list).encode('utf-8'))
    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'),
                ('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 == 'p':
            print('\n'.join(output))
        elif a == 'a':
            break

def parse_args():

    parser = optparse.OptionParser(version='%%prog %s' % VERSION,
            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('--cc', dest='cc', action='append', default=[],
                      help='specify a Cc: email recipient')
    parser.add_option('--cc-cmd',
                      help='specify a command to add whose output to the cc list')
    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('--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('--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-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)')

    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 get_first_profile():
                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
        if topic == 'master':
            print('Please use a topic branch, cannot version master branch')
            return 1

    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 options.number >= 0:
        number = options.number
    elif options.pull_request:
        number = 1
    else:
        number = get_latest_tag_number(topic) + 1

    to = options.to
    if not to and not options.edit:
        to = git_get_config_list('branch', topic, 'gitpublishto')
        if not to:
            to = get_profile_var_list(options.profile_name, 'to')
    to = set(to)

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

    cc = set(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')

    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

    profile_message_var = get_profile_var(options.profile_name, 'message')
    if options.message is not None:
        message = options.message
    elif options.pull_request:
        message = False
    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)
    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)

    invoke_hook('pre-publish-tag', base)
    # Tag the tree
    if options.pull_request:
        tag_message = ['Pull request']
        tag(tag_name_pull_request(topic), tag_message, annotate=message, force=True, sign=True)
        git_push(remote, tag_name_pull_request(topic), force=True)
    else:
        tag_message = get_latest_tag_message(topic)
        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)
            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).decode("utf-8")
                    cc = cc.union(output.splitlines())
            cc.difference_update(to)
            if inspect_emails:
                inspect_menu(tmpdir, to, cc, patches, suppress_cc,
                             options.in_reply_to, topic, options.override_cc)

            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, patches, suppress_cc, options.in_reply_to)
        except (GitError, GitSendEmailError, GitHookError, InspectEmailsError):
            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())
