#!/usr/bin/env python2
# vim: set fileencoding=utf-8 :

# Git command line interface to GitHub.
#
# Written by Leandro Lucarella <leandro.lucarella@sociomantic.com>
#
# Copyright (c) 2013 by Sociomantic Labs GmbH
#
# This program is written as a single file for practical reasons, this way
# users can download just one file and use it, while if it's spread across
# multiple modules, a setup procedure must be provided. This might change in
# the future though, if the program keeps growing.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.


"""\
Git command line interface to GitHub.

This program integrates Git and GitHub as a Git extension command (hub).
It enables many useful GitHub tasks (like creating and listing pull requests or
issues) to be carried out directly from the command line.
"""

VERSION = "git-hub devel"

import re
import sys
import time
import json
import base64
import urllib
import pprint
import urllib2
import getpass
import os.path
import urlparse
import argparse
import subprocess

# Output levels according to user selected verbosity
DEBUG = 3
INFO  = 2
WARN  = 1
ERR   = 0


# Output functions, all use the str.format() function for formatting
########################################################################

def localeprintf(stream, fmt='', *args, **kwargs):
    encoding = sys.getfilesystemencoding()
    msg = fmt.decode(encoding, 'replace').format(*args, **kwargs) + '\n'
    stream.write(msg.encode(encoding, 'replace'))

def pformat(obj):
    return pprint.pformat(obj, indent=4)

def debugf(fmt='', *args, **kwargs):
    if verbose < DEBUG:
        return
    localeprintf(sys.stdout, fmt, *args, **kwargs)

def infof(fmt='', *args, **kwargs):
    if verbose < INFO:
        return
    localeprintf(sys.stdout, fmt, *args, **kwargs)

def warnf(fmt='', *args, **kwargs):
    if verbose < WARN:
        return
    msg = ''
    if sys.stderr.isatty():
        msg += '\033[33m'
    msg += 'Warning: ' + fmt.format(*args, **kwargs)
    if sys.stderr.isatty():
        msg += '\033[0m'
    localeprintf(sys.stderr, '{}', msg)
    sys.stderr.flush()

def errf(fmt='', *args, **kwargs):
    if verbose < ERR:
        return
    msg = ''
    if sys.stderr.isatty():
        msg += '\033[31m'
    msg += 'Error: ' + fmt.format(*args, **kwargs)
    if sys.stderr.isatty():
        msg += '\033[0m'
    localeprintf(sys.stderr, '{}', msg)
    sys.stderr.flush()

def die(fmt='', *args, **kwargs):
    errf(fmt, *args, **kwargs)
    sys.exit(1)

# This is very similar to die() but used to exit the program normally having
# full stack unwinding. The message is printed with infof() and the exit status
# is 0 (normal termination). The exception is handled by the regular exception
# handling code after calling main()
def interrupt(fmt='', *args, **kwargs):
    raise InterruptException(fmt.format(*args, **kwargs))
class InterruptException (Exception):
    def __init__(self, msg):
        super(InterruptException, self).__init__(msg)

# Wrapper for raw_input to treat EOF as an empty input
def user_input(*args, **kwargs):
    try:
        return raw_input(*args, **kwargs)
    except EOFError:
        sys.stdout.write('\n')
        return ''

# Ask the user a question
#
# `question` is the text to display (information with the available options
# will be automatically appended). `options` must be an array of strings, and
# `default` a string inside that list, or None. If default is None, if the user
# press ENTER without giving an actual answer, it will be asked again,
# otherwise this function returns the `default` option.
#
# Returns the item selected by the user (one of the strings in `options`)
# unless stdin is not a tty, in which case no question is asked and this
# function returns None.
def ask(question, default=None, options=["yes", "no"]):
    if not sys.stdin.isatty():
        return None

    if default not in options and default is not None:
        raise ValueError("invalid default answer: '%s'" % default)

    valid_answers = dict()
    opts_strings = list()
    for opt in options:
        if opt is None:
            raise ValueError("options can't be None")
        valid_answers[opt.lower()] = opt
        valid_answers[opt[0].lower()] = opt
        opt_str = opt.capitalize()
        if default == opt:
            # default in bold
            opt_str = '\033[1m' + opt.upper() + '\033[21m'
        opts_strings.append(opt_str)

    # options in dark
    question += ' \033[2m[' + '/'.join(opts_strings) + '] '

    while True:
        # question in yellow
        sys.stderr.write('\033[33m' + question + '\033[0m')
        choice = user_input().lower().strip()
        if default is not None and choice == '':
            return default
        elif choice in valid_answers:
            return valid_answers[choice]
        else:
            errf("Invalid answer, valid options: {}",
                ', '.join(options))


# Git manipulation functions and constants
########################################################################

GIT_CONFIG_PREFIX = 'hub.'

# This error is thrown when a git command fails (if git prints something in
# stderr, this is considered an error even if the return code is 0). The
# `output` argument is really the stderr output only, the `output` name is
# preserved because that's how is it called in the parent class.
class GitError (subprocess.CalledProcessError):

    def __init__(self, returncode, cmd, output):
        super(GitError, self).__init__(returncode, cmd, output)

    def __str__(self):
        return '%s failed (return code: %s)\n%s' % (' '.join(self.cmd),
                self.returncode, self.output)

# Convert a variable arguments tuple to an argument list
#
# If args as only one element, the element is expected to be a string and the
# new argument list is populated by splitting the string using spaces as
# separator. If it has multiple arguments, is just converted to a list.
def args_to_list(args):
    if len(args) == 1:
        return args[0].split()
    return list(args)

# Run a git command returning its output (throws GitError on error)
#
# `args` should be strings. If only one string is found in the list, is split
# by spaces and the resulting list is used as `args`. kwargs are extra
# arguments you might want to pass to subprocess.Popen().
#
# Returns the text printed to stdout by the git command without the EOL.
def git(*args, **kwargs):
    args = args_to_list(args)
    args.insert(0, 'git')
    kwargs['stdout'] = subprocess.PIPE
    kwargs['stderr'] = subprocess.PIPE
    debugf('git command: {} {}', args, kwargs)
    proc = subprocess.Popen(args, **kwargs)
    (stdout, stderr) = proc.communicate()
    if proc.returncode != 0:
        raise GitError(proc.returncode, args, stderr.rstrip('\n'))
    return stdout.rstrip('\n')

# Check if the git version is at least min_version
# Returns a tuple with the current version and True/False depending on how the
# check went (True if it passed, False otherwise)
def git_check_version(min_version):
    cur_ver_str = git('--version').split()[2]
    cur_ver = cur_ver_str.split('.')
    min_ver = min_version.split('.')
    for i in range(len(min_ver)):
        if cur_ver[i] < min_ver[i]:
            return (cur_ver_str, False)
        if cur_ver[i] > min_ver[i]:
            return (cur_ver_str, True)
    return (cur_ver_str, True)

# Same as git() but inserts --quiet at quiet_index if verbose is less than DEBUG
def git_quiet(quiet_index, *args, **kwargs):
    args = args_to_list(args)
    if verbose < DEBUG:
        args.insert(quiet_index, '--quiet')
    return git(*args, **kwargs)

# Specialized version of git_quiet() for `push`ing that accepts an extra
# keyword argument named 'force'. If present, '--force' is passed to git push.
def git_push(*args, **kwargs):
    cmd = ['push']
    if kwargs.pop('force', False):
        cmd.append('--force')
    cmd += args_to_list(args)
    git_quiet(1, *cmd, **kwargs)

# Dummy class to indicate a value is required by git_config
class NO_DEFAULT:
    pass

# Specialized version of git() to get/set git config variables.
#
# `name` is the name of the git variable to get/set. `default` is the value
# that should be returned if variable is not defined (None will return None in
# that case, use NO_DEFAULT to make this function exit with an error if the
# variable is undefined). `prefix` is a text to prefix to the variable `name`.
# If `value` is present, then the variable is set instead. `opts` is an
# optional list of extra arguments to pass to git config.
#
# Returns the variable value (or the default if not present) if `value` is
# None, otherwise it just sets the variable.
def git_config(name, default=None, prefix=GIT_CONFIG_PREFIX, value=None, opts=()):
    name = prefix + name
    cmd = ['config'] + list(opts) + [name]
    try:
        if value is not None:
            cmd.append(value)
        return git(*cmd)
    except subprocess.CalledProcessError as e:
        if e.returncode == 1:
            if default is not NO_DEFAULT:
                return default
            die("Can't find '{}' config key in git config. "
                    "Read the man page for details.",
                    name)
        raise e

# Returns the .git directory location
def git_dir():
    return git('rev-parse', '--git-dir')


# Invokes the editor defined by git var GIT_EDITOR.
#
# The editor is invoked to edit the file .git/HUB_EDITMSG, the default content
# of that file will be the `msg` (if any) followed by the `help_msg`, used to
# hint the user about the file format.
#
# The contents of .git/HUB_EDITMSG are returned by the function (after the
# editor is closed). The text is not filtered at all at this stage (so if the
# user left the `help_msg`, it will be part of the returned string too.
def editor(help_msg, msg=None):
    prog = git('var', 'GIT_EDITOR')
    fname = os.path.join(git_dir(), 'HUB_EDITMSG')
    with file(fname, 'w') as f:
        f.write(msg or '')
        f.write(help_msg)
    status = subprocess.call([prog + ' "$@"', prog, fname], shell=True)
    if status != 0:
        die("Editor returned {}, aborting...", status)
    with file(fname) as f:
        msg = f.read()
    return msg


# git-hub specific configuration container
#
# These variables are described in the manual page.
class Config:

    def __init__(self):
        self.username = git_config('username', getpass.getuser())
        self.oauthtoken = git_config('oauthtoken')
        self.upstream = git_config('upstream')
        if self.upstream and '/' not in self.upstream:
            die("Invalid hub.upstream configuration, '/' not found")
        self.forkrepo = git_config('forkrepo')
        if not self.forkrepo and self.upstream:
            upstream = self.upstream.split('/')
            self.forkrepo = self.username + '/' + upstream[1]
        self.upstreamremote = git_config('upstreamremote', 'upstream')
        self.forkremote = git_config('forkremote', 'fork')
        self.pullbase = git_config('pullbase', 'master')
        self.urltype = git_config('urltype', 'ssh_url')
        self.baseurl = self.sanitize_url('baseurl',
            git_config('baseurl', 'https://api.github.com'))
        self.forcerebase = git_config('forcerebase', "true",
                opts=['--bool']) == "true"
        self.triangular = git_config('triangular', "true",
                opts=['--bool']) == "true"

    def sanitize_url(self, name, url):
        u = urlparse.urlsplit(url)
        name = GIT_CONFIG_PREFIX + name
        # interpret www.github.com/api/v4 as www.github.com, /api/v4
        if not u.hostname or not u.scheme:
            die("Please provide a full URL for '{}' (e.g. "
                    "https://api.github.com), got {}",
                    name, url)
        if u.username or u.password:
            warnf("Username and password in '{}' ({}) will be "
                    "ignored, use the 'setup' command "
                    "for authentication", name, url)
        netloc = u.hostname
        if u.port:
            netloc += ':' + u.port
        return urlparse.urlunsplit((u.scheme, netloc,
                u.path.rstrip('/'), u.query, u.fragment))

    def check(self, name):
        if getattr(self, name) is None:
            die("Can't find '{}{}' config key in git config. "
                    "Read the man page for details.",
                    GIT_CONFIG_PREFIX, name)


# Manages GitHub request handling authentication and content headers.
#
# The real interesting methods are created after the class declaration, for
# each type of request: head(), get(), post(), patch(), put() and delete().
#
# All these methods take an URL (relative to the config.baseurl) and optionally
# an arbitrarily number of positional or keyword arguments (but not both at the
# same time). The extra arguments, if present, are serialized as json and sent
# as the request body.
# All these methods return None if the response is empty, or the deserialized
# json data received in the body of the response.
#
# Example:
#
#    r = req.post('/repos/sociomantic-tsunami/test/labels/', name=name, color=color)
#
# The basic auth has priority over oauthtoken for authentication. If you want
# to use OAuth just leave `basic_auth` and set `oauthtoken`. To fill the
# `basic_auth` member, the `set_basic_auth()` convenient method is provided).
#
# See https://developer.github.com/ for more details on the GitHub API
class RequestManager:

    basic_auth = None
    oauthtoken = None
    links_re = re.compile(r'<([^>]+)>;.*rel=[\'"]?([^"]+)[\'"]?', re.M)

    def __init__(self, base_url, oauthtoken=None,
            username=None, password=None):
        self.base_url = base_url
        if oauthtoken is not None:
            self.oauthtoken = oauthtoken
        elif username is not None:
            self.set_basic_auth(username, password)

    # Configure the class to use basic authentication instead of OAuth
    def set_basic_auth(self, username, password):
        self.basic_auth = "Basic " + base64.urlsafe_b64encode("%s:%s" %
            (username, password))

    # Open an URL in an authenticated manner using the specified HTTP
    # method. It also add other convenience headers, like Content-Type,
    # Accept (both to json) and Content-Length).
    def auth_urlopen(self, url, method, body):
        req = urllib2.Request(url, body)
        if self.basic_auth:
            req.add_header("Authorization", self.basic_auth)
        elif self.oauthtoken:
            req.add_header("Authorization", "bearer " +
                    self.oauthtoken)
        req.add_header("Content-Type", "application/json")
        req.add_header("Accept", "application/vnd.github.v3+json")
        req.add_header("Content-Length", str(len(body) if body else 0))
        req.get_method = lambda: method.upper()
        debugf('{}', req.get_full_url())
        # Hide sensitive information from DEBUG output
        if verbose >= DEBUG:
            for h in req.header_items():
                if h[0].lower() == 'authorization':
                    debugf('{}: {}', h[0], '<hidden>')
                else:
                    debugf('{}: {}', *h)
        debugf('{}', req.get_data())
        return urllib2.urlopen(req)

    # Serialize args OR kwargs (they are mutually exclusive) as json.
    def dump(self, *args, **kwargs):
        if args and kwargs:
            raise ValueError('args and kwargs are mutually '
                'exclusive')
        if args:
            return json.dumps(args)
        if kwargs:
            return json.dumps(kwargs)
        return None

    # Get the next URL from the Link: header, if any
    def get_next_url(self, response):
        links = list()
        for l in response.headers.get("Link", "").split(','):
            links.extend(self.links_re.findall(l))
        links = dict((rel, url) for url, rel in links)
        return links.get("next", None)

    # This is the real method used to do the work of the head(), get() and
    # other high-level methods. `url` should be a relative URL for the
    # GitHub API, `method` is the HTTP method to be used (must be in
    # uppercase), and args/kwargs are data to be sent to the client. Only
    # one can be specified at a time and they are serialized as json (args
    # as a json list and kwargs as a json dictionary/object).
    def json_req(self, url, method, *args, **kwargs):
        url = self.base_url + url
        if method.upper() in ('POST', 'PATCH', 'PUT'):
            body = self.dump(*args, **kwargs)
        else:
            body = None
            url += '?' + urllib.urlencode(kwargs)
        data = None
        prev_data = None
        while url:
            debugf("Request: {} {}\n{}", method, url, body)
            res = self.auth_urlopen(url, method, body)
            data = res.read()
            debugf("Response:\n{}", data.decode('UTF8'))
            if data:
                data = json.loads(data)
                if isinstance(data, list):
                    if prev_data is None:
                        prev_data = list()
                    prev_data.extend(data)
                    data = None
            url = self.get_next_url(res)
        assert not (prev_data and data)
        if prev_data is not None:
            data = prev_data
        debugf("Parsed data:\n{}", pformat(data))
        return data

# Create RequestManager.head(), get(), ... methods
# We need the make_method() function to make Python bind the method variable
# (from the loop) early (in the loop) instead of when is called. Otherwise all
# methods get bind with the last value of method ('delete') in this case, which
# is not only what we want, is also very dangerous.
def make_method(method):
    return lambda self, url, *args, **kwargs: \
        self.json_req(url, method, *args, **kwargs)
for method in ('OPTIONS', 'HEAD', 'GET', 'POST', 'PATCH', 'PUT', 'DELETE'):
    setattr(RequestManager, method.lower(), make_method(method))


# Message cleaning and parsing functions
# (used to clean the text returned by editor())
########################################################################

def check_empty_message(msg):
    if not msg.strip():
        die("Message is empty, aborting...")
    return msg

message_markdown_help = '''\
# Remember GitHub will parse comments and descriptions as GitHub
# Flavored Markdown.
# For details see:
# https://help.github.com/articles/github-flavored-markdown/
#
# Lines starting with '# ' (note the space after the hash!) will be
# ignored, and an empty message aborts the command. The space after the
# hash is required by comments to avoid accidentally commenting out a
# line starting with a reference to an issue (#4 for example). If you
# want to include Markdown headers in your message, use the Setext-style
# headers, which consist on underlining titles with '=' for first-level
# or '-' for second-level headers.
'''

# For now it only removes comment lines
def clean_message(msg):
    lines = msg.splitlines()
    # Remove comment lines
    lines = [l for l in lines if not l.startswith('# ') and l != '#']
    return '\n'.join(lines)

# For messages expecting a title, split the first line as the title and the
# rest as the message body (but expects the title and message to be separated
# by an empty line).
#
# Returns the tuple (title, body) where body might be an empty string.
def split_titled_message(msg):
    lines = check_empty_message(clean_message(msg)).splitlines()
    title = lines[0]
    body = ''
    if len(lines) > 1:
        if lines[1].strip():
            die("Wrong message format, leave an "
                "empty line between the title "
                "and the body")
        body = '\n'.join(lines[2:])
    return (title, body)

# Perform some basic checks on the URL to avoid MitM attacks
def validate_url(url, urltype):
    scheme = urltype[:-4] # Remove the '_url'
    # Any URL starting with <transport>:: will run
    # a git-remote-<transport> command. "ext::" is specially
    # unsafe, as that can run arbitrary commands.
    if '::' in url:
        pass # Fishy already, so skip to the question
    elif scheme in ('clone', 'svn') and re.match(r'^https?://', url):
        return
    elif scheme == 'git' and re.match(r'^git://', url):
        return
    elif scheme == 'ssh' and (re.match(r'^ssh://', url) or
            re.match(r'^[^@:]+@[^:]+:', url)):
        return
    else:
        warnf("Unknown url type {}!", urltype)
    answer = ask("The URL reported by the GitHub server ({!r}) looks quite "
        "fishy! You might be under a man-in-the-middle attack "
        "(https://en.wikipedia.org/wiki/Man-in-the-middle_attack). Do "
        "you really trust your GitHub server ({})?"
            .format(url, config.baseurl),
        default='NO! ABORT!',
        options=["NO! ABORT!", "yes, it's fine"])
    if answer != "yes, it's fine":
        die("Aborted because of potential MitM attack")


# Command-line commands helper classes
########################################################################

# Base class for commands that just group other subcommands
#
# The subcommands are inspected automatically from the derived class by
# matching the names to the `subcommand_suffix`. For each sub-command, the
#  method will be called to add options to the command-line
# parser..
#
# Each sub-command is expected to have a certain structure (some methods and/or
# attributes):
#
# `run(parser, args)`
#   Required method that actually does the command work.
#   `parser` is the command-line parser instance, and `args` are the parsed
#   arguments (only the ones specific to that sub-command).
#
# `setup_parser(parser)`
#   Will be called to setup the command-line parser. This is where new
#   subcommands or specific options can be added to the parser. If it returns
#   True, then parse_known_args() will be used instead, so you can collect
#   unknown arguments (stored in args.unknown_args).
#
# `cmd_name`
#   Command name, if not present the name of the class (with the
#   `subcommand_suffix` removed and all in lowercase) is used as the name.
#
# `cmd_title`
#   A string shown in the help message when listing the group of subcommands
#   (not required but strongly recommended) for the current class.
#
# `cmd_help`
#   A string describing what this command is for (shown in the help text when
#   listing subcommands). If not present, the class __doc__ will be used.
#
# `cmd_usage`
#   A usage string to be passed to the parser. If it's not defined, is
#   generated from the options as usual. %(prog)s can be used to name the
#   current program (and subcommand).
#
# `cmd_required_config`
#   An optional list of configuration variables that this command needs to work
#   (if any of the configuration variables in this list is not defined, the
#   program exists with an error).
#
# All methods are expected to be `classmethod`s really.
class CmdGroup (object):

    subcommand_suffix = 'Cmd'

    @classmethod
    def setup_parser(cls, parser):
        partial = False
        suffix = cls.subcommand_suffix
        subcommands = [getattr(cls, a) for a in dir(cls)
                if a.endswith(suffix)]
        if not subcommands:
            return
        title = None
        if hasattr(cls, 'cmd_title'):
            title = cls.cmd_title
        subparsers = parser.add_subparsers(title=title)
        for cmd in subcommands:
            name = cmd.__name__.lower()[:-len(suffix)]
            if hasattr(cmd, 'cmd_name'):
                name = cmd.cmd_name
            help = cmd.__doc__
            if hasattr(cmd, 'cmd_help'):
                help = cmd.cmd_help
            kwargs = dict(help=help)
            if hasattr(cmd, 'cmd_usage'):
                kwargs['usage'] = cmd.cmd_usage
            p = subparsers.add_parser(name, **kwargs)
            partial = cmd.setup_parser(p) or partial
            if not hasattr(cmd, 'run'):
                continue
            if hasattr(cmd, 'cmd_required_config'):
                def make_closure(cmd):
                    def check_config_and_run(parser, args):
                        for c in cmd.cmd_required_config:
                            config.check(c)
                        cmd.run(parser, args)
                    return check_config_and_run
                p.set_defaults(run=make_closure(cmd))
            else:
                p.set_defaults(run=cmd.run)
        return partial

# `git hub setup` command implementation
class SetupCmd (object):

    cmd_help = 'perform an initial setup to connect to GitHub'

    @classmethod
    def setup_parser(cls, parser):
        parser.add_argument('-u', '--username',
            help="GitHub's username (login name). If an e-mail is "
            "provided instead, a username matching that e-mail "
            "will be searched and used instead, if found (for "
            "this to work the e-mail must be part of the public "
            "profile)")
        parser.add_argument('-p', '--password',
            help="GitHub's password (will not be stored)")
        parser.add_argument('-b', '--baseurl', metavar='URL',
            help="GitHub's base URL to use to access the API "
            "(Enterprise servers usually use https://host/api/v3)")
        group = parser.add_mutually_exclusive_group()
        group.add_argument('--global',
            dest='opts', action='store_const', const=['--global'],
            help="store settings in the global configuration "
            "(see git config --global for details)")
        group.add_argument('--system',
            dest='opts', action='store_const', const=['--system'],
            help="store settings in the system configuration "
            "(see git config --system for details)")
        parser.set_defaults(opts=[])

    @classmethod
    def run(cls, parser, args):
        is_global = ('--system' in args.opts or
                '--global' in args.opts)
        try:
            if not is_global:
                git('rev-parse --git-dir')
        except GitError as error:
            errf(error.output)
            die("Maybe you want to use --global or --system?")

        if not is_global:
            infof("Using Git repository local configuration")

        username = args.username
        password = args.password
        if (username is None or password is None) and \
                not sys.stdin.isatty():
            die("Can't perform an interactive setup outside a tty")
        if username is None:
            username = config.username or getpass.getuser()
            reply = user_input('GitHub username [%s]: ' % username)
            if reply:
                username = reply
        if password is None:
            try:
                password = getpass.getpass('GitHub '
                    'password (will not be stored): ')
            except EOFError:
                sys.stdout.write('\n')
                password = ''
        if '@' in username:
            infof("E-mail used to authenticate, trying to "
                    "retrieve the GitHub username...")
            username = cls.find_username(username)
            infof("Found: {}", username)

        req.set_basic_auth(username, password)

        note = 'git-hub'
        if not is_global and config.forkrepo:
            proj = config.forkrepo.split('/', 1)[1]
            note += ' (%s)' % proj

        while True:
            infof("Looking for GitHub authorization token...")
            auths = dict([(a['note'], a)
                for a in req.get('/authorizations')])

            if note not in auths:
                break

            errf("The OAuth token with name '{}' already exists.",
                note)
            infof("If you want to create a new one, enter a "
                "name for it. Otherwise you can go to "
                "https://github.com/settings/tokens to "
                "regenerate or delete the token '{}'", note)
            note = user_input("Enter a new token name (an empty "
                "name cancels the setup): ")

            if not note:
                sys.exit(0)

        infof("Creating auth token '{}'", note)
        auth = req.post('/authorizations', note=note,
                scopes=['user', 'repo'])

        set_config = lambda k, v: git_config(k, value=v, opts=args.opts)

        set_config('username', username)
        set_config('oauthtoken', auth['token'])
        if args.baseurl is not None:
            set_config('baseurl', args.baseurl)

    @classmethod
    def find_username(cls, name):
        users = req.get('/search/users', q=name)['items']
        users = [u['login'] for u in users]
        if not users:
            die("No users found when searching for '{}'", name)
        if len(users) > 1:
            die("More than one username found ({}), please try "
                    "again using your username instead",
                    ', '.join(users))
        return users[0].encode('UTF8')


# `git hub clone` command implementation
class CloneCmd (object):

    cmd_required_config = ['username', 'oauthtoken']
    cmd_help = 'clone a GitHub repository (and fork as needed)'
    cmd_usage = '%(prog)s [OPTIONS] [GIT CLONE OPTIONS] REPO [DEST]'

    @classmethod
    def setup_parser(cls, parser):
        parser.add_argument('repository', metavar='REPO',
            help="name of the repository to fork; in "
            "<owner>/<project> format is the upstream repository, "
            "if only <project> is specified, the <owner> part is "
            "taken from hub.username")
        parser.add_argument('dest', metavar='DEST', nargs='?',
            help="destination directory where to put the new "
            "cloned repository")
        parser.add_argument('-U', '--upstreamremote', metavar='NAME',
            default=config.upstreamremote,
            help="use NAME as the upstream remote repository name "
            "instead of the default '{}'".format(config.upstreamremote))
        parser.add_argument('-F', '--forkremote', metavar='NAME',
            default=config.forkremote,
            help="use NAME as the fork remote repository name "
            "instead of the default '{}'".format(config.forkremote))
        parser.add_argument('-t', '--triangular', action="store_true",
            default=None,
            help="use Git 'triangular workflow' setup, so you can "
            "push by default to your fork but pull by default "
            "from 'upstream'")
        parser.add_argument('--no-triangular', action="store_false",
            dest='triangular',
            help="do not use Git 'triangular workflow' setup")
        return True # we need to get unknown arguments

    @classmethod
    def run(cls, parser, args):
        (urltype, proj) = cls.parse_repo(args.repository)
        (repo, upstream, forked) = cls.setup_repo(proj)
        dest = args.dest or repo['name']
        triangular = cls.check_triangular(args.triangular
                if args.triangular is not None else config.triangular)
        if triangular and not upstream:
            parser.error("Can't use triangular workflow without "
                    "an upstream repo")
        url = repo['parent'][urltype] if triangular else repo[urltype]
        validate_url(url, urltype)
        remote = args.upstreamremote if triangular else args.forkremote
        # It's complicated to allow the user to use the --origin option, there
        # is enough complexity with --upstreamremote and --forkremote, so ask
        # the user to use those instead
        for a in args.unknown_args:
            if a in ('-o', '--origin') or a.startwith('-o', '--origin='):
                die("Please use --forkremote or --upstreamremote to name your "
                        "remotes instead of using Git's `{}` option!", a)
        # If we just forked the repo, GitHub might still be doing the actual
        # fork, so cloning could fail temporarily. See
        # https://github.com/sociomantic-tsunami/git-hub/issues/214
        cls.git_retry_if(not args.triangular and forked,
                'clone', args.unknown_args + ['--origin', remote, '--', url,
                    dest], 'Cloning {} to {}'.format(url, dest))
        if not upstream:
            # Not a forked repository, nothing else to do
            return
        # Complete the repository setup
        os.chdir(dest)
        fetchremote = args.forkremote if triangular else args.upstreamremote
        remote_url = repo['parent'][urltype]
        if triangular:
            remote_url = repo[urltype]
            git_config('remote.pushdefault', prefix='', value=fetchremote)
        git_config('upstreamremote', value=args.upstreamremote)
        git_config('forkremote', value=args.forkremote)
        git_config('urltype', value=urltype)
        git_config('upstream', value=upstream)
        validate_url(remote_url, urltype)
        git('remote', 'add', '--', fetchremote, remote_url)
        # We also need to retry in here, although is less likely since we
        # already spent some time doing the previous clone
        cls.git_retry_if(args.triangular and forked,
                'fetch', ['--', fetchremote],
                'Fetching from {} ({})'.format(fetchremote, remote_url))

    @classmethod
    def git_retry_if(cls, condition, cmd, args, progress_msg):
        # If we are not retrying, just do it once
        if not condition:
            infof(progress_msg)
            git_quiet(1, cmd, *args)
            return
        # ~5m total wait time with "human" exponential backoff
        wait_times = [1, 2, 5, 10, 15, 30, 60, 90, 90]
        retries = 0
        while retries < len(wait_times):
            try:
                infof(progress_msg)
                git_quiet(1, cmd, *args)
                break
            except GitError as e:
                t = wait_times[retries]
                warnf("Couldn't {}, maybe GitHub is still performing "
                        "the fork. Retrying in {}s ({})", cmd, t, e)
                time.sleep(t)
                retries += 1
        else:
            die("Couldn't {} after waiting ~{}m in {} retries. Maybe it's "
                "time to contact GitHub's support: https://github.com/contact",
                    cmd, sum(wait_times)//60, retries)

    @classmethod
    def parse_repo(cls, repo):
        # None means the URL was specified as just 'owner/repo' or
        # plain 'repo'
        urltype = config.urltype
        if repo.endswith('.git'):
            repo = repo[:-4] # remove suffix
        if repo.startswith('https://'):
            urltype = 'clone_url' # how GitHub calls HTTP
        elif repo.startswith('git:'):
            urltype = 'git_url'
        elif ':' in repo:
            urltype = 'ssh_url'
        # At this point we need to have an urltype
        if urltype is None:
            die("Can't infer a urltype and can't find the config "
                    "key '{}{}' config key in git config. "
                    "Read the man page for details.")
        # If no / was found, then we assume the owner is the user
        if '/' not in repo:
            repo = config.username + '/' + repo
        # Get just the owner/repo form from the full URL
        url = urlparse.urlsplit(repo)
        proj = '/'.join(url.path.split(':')[-1:][0].strip('/').split('/')[-2:])
        return (urltype, proj)

    @classmethod
    def setup_repo(cls, proj):
        forked = False
        # Own repo
        if proj.split('/')[0] == config.username:
            repo = req.get('/repos/' + proj)
            if repo['fork']:
                upstream = repo['parent']['full_name']
            else:
                upstream = None
                warnf('Repository {} is not a fork, just '
                    'cloning, upstream will not be set',
                    repo['full_name'])
        else:
            upstream = proj
            # Try to fork, if a fork already exists, we'll get the
            # information for the pre-existing fork
            # XXX: This is not properly documented in the GitHub
            #      API docs, but it seems to work as of Sep 2016.
            #      See https://github.com/sociomantic-tsunami/git-hub/pull/193
            #      for more details.
            infof('Checking for existing fork / forking...')
            repo = req.post('/repos/' + upstream + '/forks')
            infof('Fork at {}', repo['html_url'])
            forked = True
        return (repo, upstream, forked)

    @classmethod
    def check_triangular(cls, triangular):
        if not triangular:
            return False
        min_ver = '1.8.3'
        (cur_ver, ver_ok) = git_check_version(min_ver)
        if not ver_ok:
            warnf("Current git version ({}) is too old to support "
                "--triangular, at least {} is needed. Ignoring "
                "the --triangular option...", cur_ver, min_ver)
            return False
        min_ver = '1.8.4'
        (cur_ver, ver_ok) = git_check_version(min_ver)
        pd = git_config('push.default', prefix='', default=None)
        if not ver_ok and pd == 'simple':
            warnf("Current git version ({}) has an issue when "
                "using the option push.default=simple and "
                "using the --triangular workflow. Please "
                "use Git {} or newer, or change push.default "
                "to 'current' for example. Ignoring the "
                "--triangular option...", cur_ver, min_ver)
            return False
        return True


# Utility class that group common functionality used by the multiple `git hub
# issue` (and `git hub pull`) subcommands.
class IssueUtil (object):

    cmd_required_config = ['upstream', 'oauthtoken']
    # Since this class is reused by the CmdPull subcommands, we use several
    # variables to customize the out and help message to adjust to both
    # issues and pull requests.
    name = 'issue'
    gh_path = 'issues'
    id_var = 'ISSUE'
    help_msg = '''
# Please enter the title and description below.
#
# The first line is interpreted as the title. An optional description
# can follow after and empty line.
#
# Example:
#
#   Some title
#
#   Some description that can span several
#   lines.
#
''' + message_markdown_help
    comment_help_msg = '''
# Please enter your comment below.
#
''' + message_markdown_help

    @classmethod
    def print_issue_summary(cls, issue):
        infof(u'[{number}] {title} ({user[login]})\n{}{html_url}',
                u' ' * (len(str(issue['number'])) + 3),
                **issue)

    @classmethod
    def print_issue_header(cls, issue):
        issue['labels'] = ' '.join(['['+l['name']+']'
                for l in issue.get('labels', [])])
        if issue['labels']:
            issue['labels'] += '\n'
        infof(u"""
#{number}: {title}
================================================================================
{name} is {state}, was reported by {user[login]} and has {comments} comment(s).
{labels}<{html_url}>

{body}

""",
                **issue)
        if issue['comments'] > 0:
            infof(u'Comments:')

    @classmethod
    def print_issue_comment(cls, comment, indent=u''):
        body = comment['body']
        body = '\n'.join([indent+'    '+l for l in body.splitlines()])
        infof(u'{0}On {created_at}, {user[login]}, commented:\n'
            u'{0}<{html_url}>\n\n{1}\n', indent, body, **comment)

    @classmethod
    def merge_issue_comments(cls, comments, review_comments):
        hunks = dict()
        new_review_comments = list()
        for c in review_comments:
            hunk_id = (c['commit_id'], c['original_commit_id'],
                    c['position'], c['original_position'])
            if hunk_id in hunks:
                hunks[hunk_id]['_comments'].append(c)
            else:
                c['_comments'] = list()
                hunks[hunk_id] = c
                new_review_comments.append(c)
        comments = comments + new_review_comments
        comments.sort(key=lambda c: c['created_at'])
        return comments

    @classmethod
    def print_issue(cls, issue, comments, review_comments=()):
        review_comments = list(review_comments)
        issue = dict(issue)
        issue['name'] = cls.name.capitalize()
        issue['comments'] += len(comments) + len(review_comments)
        cls.print_issue_header(issue)
        for c in cls.merge_issue_comments(comments, review_comments):
            infof(u'{}\n', u'-' * 80)
            if 'diff_hunk' in c:
                hunk = '\n'.join(c['diff_hunk'].splitlines()[-5:])
                infof(u'diff --git a/{path} b/{path}\n'
                    u'index {original_commit_id}..{commit_id}\n'
                    u'--- a/{path}\n'
                    u'+++ b/{path}\n'
                    u'{0}\n',
                    hunk, **c)
                indent = '    '
                cls.print_issue_comment(c, indent)
                for cc in c['_comments']:
                    cls.print_issue_comment(cc, indent)
            else:
                cls.print_issue_comment(c)

    @classmethod
    def print_comment(cls, comment):
        body = comment['body']
        infof(u'[{id}] {}{} ({user[login]})\n{html_url}', body[:60],
                u'…' if len(body) > 60 else u'',
                u' ' * (len(str(comment['id'])) + 3),
                **comment)

    @classmethod
    def url(cls, number=None):
        s = '/repos/%s/%s' % (config.upstream, cls.gh_path)
        if number:
            s += '/' + number
        return s

    @classmethod
    def editor(cls, msg=None):
        return editor(cls.help_msg, msg)

    @classmethod
    def comment_editor(cls, msg=None):
        return editor(cls.comment_help_msg, msg)

    @classmethod
    def clean_and_post_comment(cls, issue_num, body):
        # URL fixed to issues, pull requests comments are made through
        # issues
        url = '/repos/%s/issues/%s/comments' % (config.upstream,
                issue_num)
        body = check_empty_message(clean_message(body))
        comment = req.post(url, body=body)
        cls.print_comment(comment)

# `git hub issue` command implementation
class IssueCmd (CmdGroup):

    cmd_title = 'subcommands to manage issues'
    cmd_help = 'manage issues'

    class ListCmd (IssueUtil):
        cmd_help = "show a list of open issues"
        @classmethod
        def setup_parser(cls, parser):
            parser.add_argument('-c', '--closed',
                action='store_true', default=False,
                help="show only closed %ss" % cls.name)
            parser.add_argument('-C', '--created-by-me',
                action='store_true',
                help=("show only %ss created by me" %
                        cls.name))
            parser.add_argument('-A', '--assigned-to-me',
                action='store_true',
                help=("show only %ss assigned to me" %
                        cls.name))
        @classmethod
        def run(cls, parser, args):
            def filter(issue, name):
                a = issue[name]
                if a and a['login'] == config.username:
                    return True
            state = 'closed' if args.closed else 'open'
            issues = req.get(cls.url(), state=state)
            if not issues:
                return
            if args.created_by_me and args.assigned_to_me:
                issues = [i for i in issues
                    if filter(i, 'assignee') or
                            filter(i, 'user')]
            elif args.created_by_me:
                issues = [i for i in issues
                        if filter(i, 'user')]
            elif args.assigned_to_me:
                issues = [i for i in issues
                    if filter(i, 'assignee')]
            for issue in issues:
                cls.print_issue_summary(issue)

    class ShowCmd (IssueUtil):
        cmd_help = "show details for existing issues"
        @classmethod
        def setup_parser(cls, parser):
            parser.add_argument('issues',
                nargs='+', metavar=cls.id_var,
                help="number identifying the %s to show"
                        % cls.name)
            parser.add_argument('--summary',
                default=False, action='store_true',
                help="print just a summary of the issue, not "
                "the full details with comments")
        @classmethod
        def run(cls, parser, args):
            for n in args.issues:
                issue = req.get(cls.url(n))
                if args.summary:
                    cls.print_issue_summary(issue)
                    continue
                c = []
                if issue['comments'] > 0:
                    c = req.get(cls.url(n) + "/comments")
                cls.print_issue(issue, c)

    class NewCmd (IssueUtil):
        cmd_help = "create a new issue"
        @classmethod
        def setup_parser(cls, parser):
            parser.add_argument('-m', '--message', metavar='MSG',
                help="%s's title (and description); the "
                "first line is used as the title and "
                "any text after an empty line is used as "
                "the optional body" % cls.name)
            parser.add_argument('-l', '--label', dest='labels',
                metavar='LABEL', action='append', default=[],
                help="attach LABEL to the %s (can be "
                "specified multiple times to set multiple "
                "labels)" % cls.name)
            parser.add_argument('-a', '--assign', dest='assignee',
                metavar='USER',
                help="assign a user to the %s; must be a "
                "valid GitHub login name" % cls.name)
            parser.add_argument('-M', '--milestone', metavar='ID',
                help="assign the milestone identified by the "
                "number ID to the %s" % cls.name)
        @classmethod
        def run(cls, parser, args):
            msg = args.message or cls.editor()
            (title, body) = split_titled_message(msg)
            issue = req.post(cls.url(), title=title, body=body,
                assignee=args.assignee, labels=args.labels,
                milestone=args.milestone)
            cls.print_issue_summary(issue)

    class UpdateCmd (IssueUtil):
        cmd_help = "update an existing issue"
        @classmethod
        def setup_parser(cls, parser):
            parser.add_argument('issue', metavar=cls.id_var,
                help="number identifying the %s to update"
                        % cls.name)
            parser.add_argument('-m', '--message', metavar='MSG',
                help="new %s title (and description); the "
                "first line is used as the title and "
                "any text after an empty line is used as "
                "the optional body" % cls.name)
            parser.add_argument('-e', '--edit-message',
                action='store_true', default=False,
                help="open the default $GIT_EDITOR to edit the "
                "current title (and description) of the %s"
                        % cls.name)
            group = parser.add_mutually_exclusive_group()
            group.add_argument('-o', '--open', dest='state',
                action='store_const', const='open',
                help="reopen the %s" % cls.name)
            group.add_argument('-c', '--close', dest='state',
                action='store_const', const='closed',
                help="close the %s" % cls.name)
            parser.add_argument('-l', '--label', dest='labels',
                metavar='LABEL', action='append',
                help="if one or more labels are specified, "
                "they will replace the current %s labels; "
                "otherwise the labels are unchanged. If one of "
                "the labels is empty, the labels will be "
                "cleared (so you can use -l'' to clear the "
                "labels)" % cls.name)
            parser.add_argument('-a', '--assign', dest='assignee',
                metavar='USER',
                help="assign a user to the %s; must be a "
                "valid GitHub login name" % cls.name)
            parser.add_argument('-M', '--milestone', metavar='ID',
                help="assign the milestone identified by the "
                "number ID to the %s" % cls.name)
        @classmethod
        def run(cls, parser, args):
            # URL fixed to issues, pull requests updates are made
            # through issues to allow changing labels, assignee and
            # milestone (even when GitHub itself doesn't support it
            # :D)
            url = '/repos/%s/issues/%s' % (config.upstream,
                    args.issue)
            params = dict()
            # Should labels be cleared?
            if (args.labels and len(args.labels) == 1 and
                    not args.labels[0]):
                params['labels'] = []
            elif args.labels:
                params['labels'] = args.labels
            if args.state:
                params['state'] = args.state
            if args.assignee is not None:
                params['assignee'] = args.assignee
            if args.milestone is not None:
                params['milestone'] = args.milestone
            msg = args.message
            if args.edit_message:
                if not msg:
                    issue = req.get(url)
                    msg = issue['title']
                    if issue['body']:
                        msg += '\n\n' + issue['body']
                msg = cls.editor(msg)
            if msg:
                (title, body) = split_titled_message(msg)
                params['title'] = title
                params['body'] = body
            issue = req.patch(url, **params)
            cls.print_issue_summary(issue)

    class CommentCmd (IssueUtil):
        cmd_help = "add a comment to an existing issue"
        @classmethod
        def setup_parser(cls, parser):
            parser.add_argument('issue', metavar=cls.id_var,
                help="number identifying the %s to comment on"
                        % cls.name)
            parser.add_argument('-m', '--message', metavar='MSG',
                help="comment to be added to the %s; if "
                "this option is not used, the default "
                "$GIT_EDITOR is opened to write the comment"
                        % cls.name)
        @classmethod
        def run(cls, parser, args):
            body = args.message or cls.comment_editor()
            cls.clean_and_post_comment(args.issue, body)

    class CloseCmd (IssueUtil):
        cmd_help = "close an opened issue"
        @classmethod
        def setup_parser(cls, parser):
            parser.add_argument('issue', metavar=cls.id_var,
                help="number identifying the %s to close"
                        % cls.name)
            parser.add_argument('-m', '--message', metavar='MSG',
                help="add a comment to the %s before "
                "closing it" % cls.name)
            parser.add_argument('-e', '--edit-message',
                action='store_true', default=False,
                help="open the default $GIT_EDITOR to write "
                "a comment to be added to the %s before "
                "closing it" % cls.name)
        @classmethod
        def run(cls, parser, args):
            msg = args.message
            if args.edit_message:
                msg = cls.comment_editor(msg)
            if msg:
                cls.clean_and_post_comment(args.issue, msg)
            issue = req.patch(cls.url(args.issue), state='closed')
            cls.print_issue_summary(issue)


# Utility class that group common functionality used by the multiple `git hub
# pull`) subcommands specifically.
class PullUtil (IssueUtil):

    name = 'pull request'
    gh_path = 'pulls'
    id_var = 'PULL'
    rebase_msg = 'This pull request has been rebased via ' \
            '`git hub pull rebase`. Original pull request HEAD ' \
            'was {}, new (rebased) HEAD is {}'

    @classmethod
    def get_ref(cls, ref='HEAD'):
        ref_hash = git('rev-parse ' + ref)
        ref_name = git('rev-parse --abbrev-ref ' + ref)
        if not ref_name or ref_name == 'HEAD' or ref_hash == ref_name:
            ref_name = None
        return ref_hash, ref_name

    @classmethod
    def tracking_branch(cls, head):
        if head is None:
            return None
        ref = git_config('branch.%s.merge' % head, prefix='')
        if ref is None:
            return None
        # the format is usually a full reference specification, like
        # "refs/heads/<branch>", we just assume the user is always
        # using a branch
        return ref.split('/')[-1]

    # push head to remote_head only if is necessary
    @classmethod
    def push(cls, head, remote_head, force):
        local_hash = git('rev-parse', head)
        remote_hash = 'x' # dummy variable that doesn't match any git hash
        remote_branch = '%s/%s' % (config.forkremote, head)
        if cls.branch_exists(remote_branch):
            remote_hash = git('rev-parse', remote_branch)
        if local_hash != remote_hash:
            infof('Pushing {} to {} in {}', head, remote_head,
                    config.forkremote)
            git_push(config.forkremote,
                    head+':refs/heads/'+remote_head,
                    force=force)

    @classmethod
    def branch_exists(cls, branch):
        status = subprocess.call('git rev-parse --verify --quiet ' +
                'refs/heads/' + branch + ' > /dev/null',
                shell=True)
        return status == 0

    @classmethod
    def get_default_branch_msg(cls, branch_ref, branch_name):
        if branch_name is not None:
            msg = git_config('branch.%s.description' % branch_name,
                    '', '')
            if msg:
                return msg
        return git('log -1 --pretty=format:%s%n%n%b ' + branch_ref)

    @classmethod
    def get_local_remote_heads(cls, parser, args):
        head_ref, head_name = cls.get_ref(args.head or 'HEAD')
        remote_head = args.create_branch or head_name
        if not remote_head:
            die("Can't guess remote branch name, please "
                "use --create-branch to specify one")
        base = args.base or cls.tracking_branch(head_name) or \
                config.pullbase
        gh_head = config.forkrepo.split('/')[0] + ':' + remote_head
        return head_ref, head_name, remote_head, base, gh_head

    # Perform a validated git fetch
    @classmethod
    def git_fetch(cls, url, ref):
        validate_url(url, config.urltype)
        infof('Fetching {} from {}', ref, url)
        git_quiet(1, 'fetch', '--', url, ref)

# `git hub pull` command implementation
class PullCmd (IssueCmd):

    cmd_title = 'subcommands to manage pull requests'
    cmd_help = 'manage pull requests'

    # Most of the commands are just aliases to the git hub issue commands.
    # We derive from the PullUtil first to get the pull specific variables
    # (name, gh_path, id_var) with higher priority than the ones in the
    # IssueCmd subcommands.
    class ListCmd (PullUtil, IssueCmd.ListCmd):
        cmd_help = "show a list of open pull requests"
        pass

    class ShowCmd (PullUtil, IssueCmd.ShowCmd):
        cmd_help = "show details for existing pull requests"
        @classmethod
        def run(cls, parser, args):
            for n in args.issues:
                pull = req.get(cls.url(n))
                if args.summary:
                    cls.print_issue_summary(pull)
                    continue
                # Damn GitHub doesn't provide labels for
                # pull request objects
                issue_url = '/repos/%s/issues/%s' % (
                        config.upstream, n)
                issue = req.get(issue_url)
                pull['labels'] = issue.get('labels', [])
                c = []
                if pull['comments'] > 0:
                    c = req.get(issue_url + '/comments')
                ic = []
                if pull['review_comments'] > 0:
                    ic = req.get(cls.url(n) + "/comments")
                cls.print_issue(pull, c, ic)

    class UpdateCmd (PullUtil, IssueCmd.UpdateCmd):
        cmd_help = "update an existing pull request"
        pass

    class CommentCmd (PullUtil, IssueCmd.CommentCmd):
        cmd_help = "add a comment to an existing pull request"
        pass

    class CloseCmd (PullUtil, IssueCmd.CloseCmd):
        cmd_help = "close an opened pull request"
        pass

    class NewCmd (PullUtil):
        cmd_help = "create a new pull request"
        @classmethod
        def setup_parser(cls, parser):
            parser.add_argument('head', metavar='HEAD', nargs='?',
                help="branch (or git ref) where your changes "
                "are implemented")
            parser.add_argument('-m', '--message', metavar='MSG',
                help="pull request title (and description); "
                "the first line is used as the pull request "
                "title and any text after an empty line is "
                "used as the optional body")
            parser.add_argument('-b', '--base', metavar='BASE',
                help="branch (or git ref) you want your "
                "changes pulled into (uses the tracking "
                "branch by default, or hub.pullbase if "
                "there is none, or 'master' as a fallback)")
            parser.add_argument('-c', '--create-branch',
                metavar='NAME',
                help="create a new remote branch with NAME "
                "as the real head for the pull request instead "
                "of using the HEAD name passed as 'head'")
            parser.add_argument('-f', '--force-push',
                action='store_true', default=False,
                help="force the push git operation (use with "
                "care!)")
        @classmethod
        def run(cls, parser, args):
            head_ref, head_name, remote_head, base, gh_head = \
                    cls.get_local_remote_heads(parser, args)
            msg = args.message
            if not msg:
                msg = cls.editor(cls.get_default_branch_msg(
                        head_ref, head_name))
            (title, body) = split_titled_message(msg)
            cls.push(head_name or head_ref, remote_head,
                    force=args.force_push)
            infof("Creating pull request from branch {} to {}:{}",
                    remote_head, config.upstream, base)
            pull = req.post(cls.url(), head=gh_head, base=base,
                    title=title, body=body)
            cls.print_issue_summary(pull)

    class AttachCmd (PullUtil):
        cmd_help = "attach code to an existing issue (convert it " \
                "to a pull request)"
        @classmethod
        def setup_parser(cls, parser):
            parser.add_argument('issue', metavar='ISSUE',
                help="pull request ID to attach code to")
            parser.add_argument('head', metavar='HEAD', nargs='?',
                help="branch (or git ref) where your changes "
                "are implemented")
            parser.add_argument('-m', '--message', metavar='MSG',
                help="add a comment to the new pull request")
            parser.add_argument('-e', '--edit-message',
                action='store_true', default=False,
                help="open the default $GIT_EDITOR to write "
                "a comment to be added to the pull request "
                "after attaching the code to it")
            parser.add_argument('-b', '--base', metavar='BASE',
                help="branch (or git ref) you want your "
                "changes pulled into (uses the tracking "
                "branch by default, or hub.pullbase if "
                "there is none, or 'master' as a fallback)")
            parser.add_argument('-c', '--create-branch',
                metavar='NAME',
                help="create a new remote branch with NAME "
                "as the real head for the pull request instead "
                "of using the HEAD name passed as 'head'")
            parser.add_argument('-f', '--force-push',
                action='store_true', default=False,
                help="force the push git operation (use with "
                "care!)")
        @classmethod
        def run(cls, parser, args):
            head_ref, head_name, remote_head, base, gh_head = \
                    cls.get_local_remote_heads(parser, args)
            msg = args.message
            if args.edit_message:
                if not msg:
                    msg = cls.get_default_branch_msg(
                            head_ref, head_name)
                msg = cls.comment_editor(msg)
            cls.push(head_name or head_ref, remote_head,
                    force=args.force_push)
            infof("Attaching commits in branch {} to issue #{} "
                    "(to be merged to {}:{})", remote_head,
                    args.issue, config.upstream, base)
            pull = req.post(cls.url(), issue=args.issue, base=base,
                    head=gh_head, maintainer_can_modify=False)
            cls.print_issue_summary(pull)
            if msg:
                cls.clean_and_post_comment(args.issue, msg)

    class CheckoutCmd (PullUtil):
        cmd_help = "checkout the remote branch (head) of the pull request"
        @classmethod
        def setup_parser(cls, parser):
            parser.add_argument('pull',
                help="number identifying the pull request to checkout")
            parser.add_argument("args",
                nargs=argparse.REMAINDER,
                help="any extra arguments to pass to `git checkout`")
        @classmethod
        def run(cls, parser, args):
            pull = req.get(cls.url(args.pull))

            if pull['state'] == 'closed':
                warnf('Checking out a closed pull request '
                    '(closed at {closed_at})!', **pull)

            remote_url = pull['base']['repo'][config.urltype]
            remote_ref = 'pull/{}/head'.format(pull['number'])

            cls.git_fetch(remote_url, remote_ref)
            git_quiet(1, 'checkout', 'FETCH_HEAD', *args.args)

# This class is top-level just for convenience, because is too big. Is added to
# PullCmd after is completely defined!
#
# This command is by far the most complex part of this program. Since a rebase
# can fail (because of conflicts) and the user gets a prompt back, there are
# millions of possible situations when we try to resume the rebase. For this
# reason this command is divided in several small methods that are reused as
# much as possible (usually by using the command-line option `action` to figure
# out what to do next).
#
# Error (exception) handling in these methods is EXTREMELY important too. If
# anything fails badly, we need to restore the user repo to its original state,
# but if the failure is because of conflicts, we only have to do partial
# cleanup (or no cleanup at all). For this reason is the class variable
# `in_conflict` defined. When we are recovering from a conflict, this flag is
# set to true so the normal cleanup is not done (or done partially).
class RebaseCmd (PullUtil):
    cmd_help = "close a pull request by rebasing its base branch"

    stash_msg_base = "stashed by git hub pull rebase"

    in_conflict = False

    # These variables are stored in the .git/HUB_PULL_REBASING file if the
    # rebasing was interrupted due to conflicts. When the rebasing is
    # resumed (via --continue or --skip) these variables are loaded from
    # that file.
    saved_old_ref = None
    saved_message = None
    saved_edit_msg = None
    saved_pause = None
    saved_delete_branch = None
    saved_is_merge = None
    # this variable is a bit different, as is read/write by
    # read_rebasing_file()/create_rebasing_file() directly. This is not
    # ideal and should be addressed when #35 is fixed.
    in_pause = False

    @classmethod
    def setup_parser(cls, parser):
        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument('pull',
            metavar=cls.id_var, nargs='?',
            help="number identifying the pull request to rebase")
        group.add_argument('--continue', dest='action',
            action='store_const', const='--continue',
            help="continue an ongoing rebase")
        group.add_argument('--abort', dest='action',
            action='store_const', const='--abort',
            help="abort an ongoing rebase")
        group.add_argument('--skip', dest='action',
            action='store_const', const='--skip',
            help="skip current patch and continue")
        parser.add_argument('-m', '--message', metavar='MSG',
            help="add a comment to the pull request before closing "
            "it; if not specified a default comment is added (to "
            "avoid adding a comment at all use -m'')")
        parser.add_argument('-e', '--edit-message',
            action='store_true', default=False,
            help="open the default $GIT_EDITOR to edit the comment "
            "to be added to the pull request before closing it")
        parser.add_argument('--force-push',
            action='store_true', default=False,
            help="force the push git operation (use with care!)")
        parser.add_argument('-p', '--pause',
            action='store_true', default=False,
            help="pause the rebase just before the results are "
            "pushed (useful for testing)")
        parser.add_argument('-u', '--stash-include-untracked',
            action='store_true', default=False,
            help="uses git stash save --include-untracked when "
            "stashing local changes")
        parser.add_argument('-a', '--stash-all',
            action='store_true', default=False,
            help="uses git stash save --all when stashing local "
            "changes")
        parser.add_argument('-D', '--delete-branch',
            action='store_true', default=False,
            help="removes the PR branch, like the Delete "
            "Branch button (TM)")

    @classmethod
    def run(cls, parser, args):
        ongoing_rebase_pull_id = cls.read_rebasing_file()
        if args.pull is not None and ongoing_rebase_pull_id is not None:
            die("Another pull rebase is in progress, can't start "
                "a new one")
        if (args.pull is None and ongoing_rebase_pull_id is None and
                    args.action != '--abort'):
            die("Can't {}, no pull rebase is in progress",
                args.action)

        if args.pull is not None:
            if cls.rebasing() or cls.merging():
                die("Can't start a pull rebase while a "
                    "regular rebase or merge is in progress")
            cls.start_rebase(args)
        else:
            args.pull = ongoing_rebase_pull_id
            if args.message is None:
                args.message = cls.saved_message
            if not args.edit_message:
                args.edit_message = cls.saved_edit_msg
            if not args.pause:
                args.pause = cls.saved_pause
            if not args.delete_branch:
                args.delete_branch = cls.saved_delete_branch

            if args.action == '--abort':
                cls.abort_rebase(args)
            else:
                cls.check_continue_rebasing(args)
                cls.start_rebase(args)

    # Check if we are able to continue an ongoing rebase (if we can't for
    # any reason, we quit, this function only returns on success). On some
    # conditions the user is asked about what to do.
    @classmethod
    def check_continue_rebasing(cls, args):
        if cls.rebasing() or cls.merging():
            return
        head_ref, head_name = cls.get_ref()
        if args.action == '--continue' and \
                cls.get_tmp_ref(args.pull) == head_name:
            return
        answer = ask("No rebase in progress found for this pull "
            "rebase, do you want to continue as if the rebase was "
            "successfully finished, abort the rebasing cancelling "
            "the whole rebase or just quit?", default="quit",
            options=['continue', 'abort', 'quit'])
        if answer is None:
            die("No rebase in progress found for this "
                "pull rebase, don't know how to proceed")
        if answer == 'abort':
            cls.abort_rebase(args)
            sys.exit(0)

    # Abort an ongoing rebase by trying to return to the state the repo was
    # before the rebasing started (reset to the previous head, remove
    # temporary branches, restore the stashed changes, etc.).
    @classmethod
    def abort_rebase(cls, args):
        aborted = cls.force_rebase_abort()
        if args.pull is not None and cls.saved_old_ref is not None:
            cls.clean_ongoing_rebase(args.pull, cls.saved_old_ref,
                    warnf)
            aborted = True
        aborted = cls.remove_rebasing_file() or aborted
        aborted = cls.pop_stashed() or aborted
        if not aborted:
            die("Nothing done, maybe there isn't an ongoing rebase "
                "of a pull request?")

    # Tries to (restart a rebase (or continue it, depending on the
    # args.action). If is starting a new rebase, it stash any local changes
    # and creates a branch, rebase, etc.
    @classmethod
    def start_rebase(cls, args):
        starting = args.action is None
        pull = cls.get_pull(args.pull, starting)
        if starting:
            cls.stash(pull, args)
        try:
            pushed_sha = cls.fetch_rebase_push(args, pull)
        finally:
            if not cls.in_conflict and not cls.in_pause:
                cls.pop_stashed()
        try:
            pull = cls.update_github(args, pull, pushed_sha)
        except (OSError, IOError) as e:
            errf("GitHub information couldn't be updated "
                "correctly, but the pull request was "
                "successfully rebased ({}).", e)
            raise e
        finally:
            cls.print_issue_summary(pull)

    # Get the pull request object from GitHub performing some sanity checks
    # (if it's already merged, or closed or in a mergeable state). If
    # the state is not the ideal, it asks the user how to proceed.
    #
    # If `check_all` is False, the only check performed is if it is already
    # merged).
    #
    # If the pull request can't be merged (or the user decided to cancel),
    # this function terminates the program (it returns only on success).
    @classmethod
    def get_pull(cls, pull_id, check_all):
        pull = req.get(cls.url(pull_id))
        if pull['merged']:
            infof("Nothing to do, already merged (--abort to get "
                    "back to normal)")
            sys.exit(0)
        if not check_all:
            return pull
        if pull['state'] == 'closed':
            answer = ask("The pull request is closed, are you sure "
                "you want to rebase it?", default="no")
            if answer is None:
                die("Can't rebase/merge, pull request is closed")
            elif answer == 'no':
                sys.exit(0)
        if not pull['mergeable']:
            answer = ask("The pull request is not in a mergeable "
                "state (there are probably conflicts to "
                "resolve), do you want to continue anyway?",
                default="no")
            if answer is None:
                die("Can't continue, the pull request isn't "
                    "in a mergeable state")
            elif answer == 'no':
                sys.exit(0)
        # Check status
        status = req.get('/repos/%s/commits/%s/status' %
                (config.upstream, pull['head']['sha']))
        state = status['state']
        statuses = status['statuses']
        if len(statuses) > 0 and state != 'success':
            url = statuses[0]['target_url']
            answer = ask("The current pull request status is '%s' "
                "(take a look at %s for more information), "
                "do you want to continue anyway?" %
                (state, url), default="no")
            if answer is None:
                die("Can't continue, the pull request status "
                    "is '{}' (take a look at {} for more "
                    "information)", state, url)
            elif answer == 'no':
                sys.exit(0)
        return pull

    # Returns the name of the temporary branch to work on while doing the
    # rebase
    @classmethod
    def get_tmp_ref(cls, pull_id):
        return 'git-hub-pull-rebase-%s' % pull_id

    # Pop stashed changes (if any). Warns (but doesn't pop the stashed
    # changes) if they are present in the stash, but not in the top.
    #
    # Returns True if it was successfully popped, False otherwise.
    @classmethod
    def pop_stashed(cls):
        stashed_index = cls.get_stashed_state()
        if stashed_index == 0:
            git_quiet(2, 'stash', 'pop')
            return True
        elif stashed_index > 0:
            warnf("Stash produced by this command found "
                "(stash@{{}}) but not as the last stashed "
                "changes, leaving the stashed as it is",
                stashed_index)
        return False

    # Returns the index of the stash created by this program in the stash
    # in the stack, or None if not present at all. 0 means is the latest
    # stash in the stash stack.
    @classmethod
    def get_stashed_state(cls):
        stash_msg_re = re.compile(r'.*' + cls.stash_msg_base + r' \d+')
        stashs = git('stash', 'list').splitlines()
        for i, stash in enumerate(stashs):
            if stash_msg_re.match(stash):
                return i
        return None

    # Returns a string with the message to use when stashing local changes.
    @classmethod
    def stash_msg(cls, pull):
        return '%s %s' % (cls.stash_msg_base, pull['number'])

    # Do a git stash using the required options
    @classmethod
    def stash(cls, pull, args):
        git_args = [2, 'stash', 'save']
        if args.stash_include_untracked:
            git_args.append('--include-untracked')
        if args.stash_all:
            git_args.append('--all')
        git_args.append(cls.stash_msg(pull))
        try:
            git_quiet(*git_args)
        except GitError as e:
            errf("Couldn't stash current changes, try with "
                "--stash-include-untracked or --stash-all "
                "if you had conflicts")
            raise e

    # Performs the whole rebasing procedure, including fetching the branch
    # to be rebased, creating a temporary branch for it, fetching the base
    # branch, rebasing to the base branch and pushing the results.
    #
    # The number of operations performed can vary depending on the
    # `args.action` (i.e. if we are --continue'ing or --skip'ping
    # a rebase). If the rebase is being continued, only the steps starting
    # with the rebasing itself are performed.
    #
    # Returns the new HEAD hash after the rebase is completed.
    @classmethod
    def fetch_rebase_push(cls, args, pull):
        if pull['head']['repo'] is None:
            die("It seems like the repository referenced by "
                "this pull request has been deleted")
        starting = args.action is None
        head_url = pull['head']['repo'][config.urltype]
        head_ref = pull['head']['ref']
        base_url = pull['base']['repo'][config.urltype]
        base_ref = pull['base']['ref']
        tmp_ref = cls.get_tmp_ref(pull['number'])
        old_ref = cls.saved_old_ref
        is_merge = cls.saved_is_merge
        if old_ref is None:
            old_ref_ref, old_ref_name = cls.get_ref()
            old_ref = old_ref_name or old_ref_ref

        if starting:
            cls.git_fetch(head_url, head_ref)
            head_hash = git('rev-parse FETCH_HEAD')
            cls.git_fetch(base_url, base_ref)
            base_hash = git('rev-parse FETCH_HEAD')
            parents = git('show --quiet --format=%P ' + head_hash).split()
            is_merge = len(parents) > 1
            # Last commit is a merge commit, so ask the user to merge instead
            if is_merge:
                answer = ask("The last commit in the pull request is a merge "
                    "commit, use the merge instead of doing a rebase?",
                    default="yes")
                if answer == 'no':
                    is_merge = False
        try:
            if starting:
                git_quiet(1, 'checkout', '-b', tmp_ref,
                        base_hash if is_merge else head_hash)
                infof('Rebasing to {} in {}', base_ref, base_url)
                cls.create_rebasing_file(pull, args, old_ref, is_merge)
            # Only run the rebase if we are not continuing with
            # a pull rebase that the user finished rebasing using
            # a plain git rebase --continue
            if starting or cls.rebasing() or cls.merging():
                cls.rebase(args, pull, is_merge, base_hash, head_hash)
            if args.pause and not cls.in_pause:
                cls.in_pause = True
                # Re-create the rebasing file as we are going
                # to pause
                cls.remove_rebasing_file()
                cls.create_rebasing_file(pull, args, old_ref, is_merge)
                interrupt("Rebase done, now --pause'ing. "
                    'Use --continue {}when done.',
                    '' if starting else 'once more ')
            # If we were paused, remove the rebasing file that we
            # just re-created
            if cls.in_pause:
                cls.in_pause = False
                cls.remove_rebasing_file()
            infof('Pushing results to {} in {}',
                    base_ref, base_url)
            git_push(base_url, 'HEAD:' + base_ref,
                    force=args.force_push)
            if args.delete_branch:
                infof('Removing pull request branch {} in {}',
                        head_ref, head_url)
                try:
                    git_push(head_url, ':' + head_ref,
                            force=args.force_push)
                except GitError as e:
                    warnf('Error removing branch: {}', e)
            return git('rev-parse HEAD')
        finally:
            if not cls.in_conflict and not cls.in_pause:
                cls.clean_ongoing_rebase(pull['number'], old_ref)

    # Reverts the operations done by fetch_rebase_push().
    @classmethod
    def clean_ongoing_rebase(cls, pull_id, old_ref, errfunc=die):
        tmp_ref = cls.get_tmp_ref(pull_id)
        git_quiet(1, 'reset', '--hard')
        try:
            git_quiet(1, 'checkout', old_ref)
        except subprocess.CalledProcessError as e:
            errfunc("Can't checkout '{}', maybe it was removed "
                "during the rebase? {}",  old_ref, e)
        if cls.branch_exists(tmp_ref):
            git('branch', '-D', tmp_ref)

    # Performs the rebasing itself. Sets the `in_conflict` flag if
    # a conflict is detected.
    @classmethod
    def rebase(cls, args, pull, is_merge, base_hash, head_hash):
        starting = args.action is None
        try:
            if starting:
                # Last commit is a merge commit, so merge --ff-only instead
                if is_merge:
                    git_quiet(1, 'merge --ff-only ' + head_hash)
                    return
                a = []
                if config.forcerebase:
                    a.append('--force')
                a.append(base_hash)
                git_quiet(1, 'rebase', *a)
            else:
                git('rebase', args.action)
        except subprocess.CalledProcessError as e:
            if e.returncode == 1 and (cls.rebasing() or cls.merging()):
                cls.in_conflict = True
                die("Conflict detected, resolve "
                    "conflicts and run git hub "
                    "pull rebase --continue to "
                    "proceed")
            raise e
        finally:
            if not cls.in_conflict:
                # Always try to abort the rebasing, in case
                # there was an error
                cls.remove_rebasing_file()
                cls.force_rebase_abort()

    # Run git rebase --abort without complaining if it fails.
    @classmethod
    def force_rebase_abort(cls):
        try:
            git('rebase' if cls.rebasing() else 'merge', '--abort',
                    stderr=subprocess.STDOUT)
        except subprocess.CalledProcessError:
            return False
        return True

    # Do all the GitHub part of the rebasing (closing the rebase, adding
    # a comment including opening the editor if needed to get the message).
    @classmethod
    def update_github(cls, args, pull, pushed_sha):
        pull_sha = pull['head']['sha']
        msg = args.message
        if msg is None and pull_sha != pushed_sha:
            msg = cls.rebase_msg.format(pull_sha, pushed_sha)
        if args.edit_message:
            msg = cls.comment_editor(msg)
        if msg:
            cls.clean_and_post_comment(args.pull, msg)
        pull = req.get(cls.url(args.pull))
        if pull['state'] == 'open' and pull_sha != pushed_sha:
            pull = req.patch(cls.url(args.pull),
                    state='closed')
        return pull

    # Returns True if there is a (pure) `git rebase` going on (not
    # necessarily a `git hub pull rebase`).
    @classmethod
    def rebasing(cls):
        return os.path.exists(git_dir()+'/rebase-apply/rebasing')

    # Returns True if there is a `git merge` going on
    @classmethod
    def merging(cls):
        return os.path.exists(git_dir()+'/MERGE_HEAD')

    # Returns the file name used to store `git hub pull rebase` metadata
    # (the sole presence of this file indicates there is a `git hub pull
    # rebase` going on).
    @classmethod
    def rebasing_file_name(cls):
        return os.path.join(git_dir(), 'HUB_PULL_REBASING')

    # Reads and parses the contents of the `rebasing_file`, returning them
    # as variables, if the file exists.
    #
    # If the file exists, the class variables `saved_old_ref`,
    # `saved_edit_msg` and `saved_message` are filled with the file
    # contents and the pull request ID that's being rebased is returned.
    # Otherwise it just returns None and leaves the class variables alone.
    @classmethod
    def read_rebasing_file(cls):
        fname = cls.rebasing_file_name()
        if os.path.exists(fname):
            try:
                with file(fname) as f:
                    # id read as string
                    pull_id = f.readline()[:-1] # strip \n
                    cls.saved_old_ref = f.readline()[:-1]
                    assert cls.saved_old_ref
                    pause = f.readline()[:-1]
                    cls.saved_pause = (pause == "True")
                    delete_branch = f.readline()[:-1]
                    cls.saved_delete_branch = (delete_branch == "True")
                    in_pause = f.readline()[:-1]
                    cls.in_pause = (in_pause == "True")
                    edit_msg = f.readline()[:-1]
                    cls.saved_edit_msg = (edit_msg == "True")
                    is_merge = f.readline()[:-1]
                    cls.saved_is_merge = (is_merge == "True")
                    msg = f.read()
                    if msg == '\n':
                        msg = ''
                    elif not msg:
                        msg = None
                    cls.saved_message = msg
                    return pull_id
            except EnvironmentError as e:
                die("Error reading pull rebase information "
                    "file '{}': {}", fname, e)
        return None

    # Creates the `rebasing_file` storing: the `pull` ID, the `old_ref`
    # (the hash of the HEAD commit before the rebase was started),
    # the `args.edit_message` flag and the `args.message` text. It fails
    # (and exits) if the file was already present.
    @classmethod
    def create_rebasing_file(cls, pull, args, old_ref, is_merge):
        fname = cls.rebasing_file_name()
        try:
            fd = os.open(cls.rebasing_file_name(),
                    os.O_WRONLY | os.O_CREAT | os.O_EXCL,
                    0o777)
            with os.fdopen(fd, 'w') as f:
                # id written as string
                f.write(str(pull['number']) + '\n')
                f.write(old_ref + '\n')
                f.write(repr(args.pause) + '\n')
                f.write(repr(args.delete_branch) + '\n')
                f.write(repr(cls.in_pause) + '\n')
                f.write(repr(args.edit_message) + '\n')
                f.write(repr(is_merge) + '\n')
                if (args.message is not None):
                    f.write(args.message + '\n')
        except EnvironmentError as e:
            die("Error writing pull rebase information "
                "file '{}': {}", fname, e)

    # Removes the `rebasing_file` from the filesystem.
    #
    # Returns True if it was successfully removed, False if it didn't exist
    # and terminates the program if there is another I/O error.
    @classmethod
    def remove_rebasing_file(cls):
        fname = cls.rebasing_file_name()
        if not os.path.exists(fname):
            return False
        try:
            os.unlink(fname)
        except EnvironmentError as e:
            die("Error removing pull rebase information "
                "file '{}': {}", fname, e)
        return True
# and we finally add it to the PullCmd class as a member
PullCmd.RebaseCmd = RebaseCmd


# `git hub` command implementation
class HubCmd (CmdGroup):
    cmd_title = "subcommands"
    cmd_help = "git command line interface to GitHub"
    SetupCmd = SetupCmd
    CloneCmd = CloneCmd
    IssueCmd = IssueCmd
    PullCmd = PullCmd


def main():
    global args, config, req, verbose

    verbose = INFO
    config = Config()

    parser = argparse.ArgumentParser(
            description='Git command line interface to GitHub')
    parser.add_argument('--version', action='version', version=VERSION)
    parser.add_argument('-v', '--verbose', action='count', default=INFO,
        help="be more verbose (can be specified multiple times to get "
        "extra verbosity)")
    parser.add_argument('-s', '--silent', action='count', default=0,
        help="be less verbose (can be specified multiple times to get "
        "less verbosity)")
    partial = HubCmd.setup_parser(parser)
    if partial:
        args, unknown_args = parser.parse_known_args()
        args.unknown_args = unknown_args
    else:
        args = parser.parse_args()
    verbose = args.verbose - args.silent

    req = RequestManager(config.baseurl, config.oauthtoken)

    # Temporary warning to note the configuration variable changes
    if git_config('password') is not None:
        warnf('It looks like your {0}password configuration '
            'variable is set.\nThis variable is not used '
            'anymore, you might want to delete it.\nFor example: '
            'git config --global --unset {0}password',
            GIT_CONFIG_PREFIX)

    args.run(parser, args)


# Entry point of the program, just calls main() and handles errors.
if __name__ == '__main__':
    try:
        main()
    except urllib2.HTTPError as error:
        try:
            body = error.read()
            err = json.loads(body)
            prefix = 'GitHub error: '
            error_printed = False
            if 'message' in err:
                errf('{}{message}', prefix, **err)
                error_printed = True
            if 'errors' in err:
                for e in err['errors']:
                    if 'message' in err:
                        errf('{}{message}',
                            ' ' * len(prefix), **e)
                        error_printed = True
            if not error_printed:
                errf('{} for {}: {}', error, error.geturl(), body)
            else:
                debugf('{}', error)
            debugf('{}', error.geturl())
            debugf('{}', pformat(error.headers))
            debugf('{}', error.read())
        except:
            errf('{}', error)
            errf('{}', error.geturl())
            errf('{}', pformat(error.headers))
            errf('{}', body)
            sys.exit(3)
        sys.exit(4)
    except urllib2.URLError as error:
        errf('Network error: {}', error)
        sys.exit(5)
    except KeyboardInterrupt:
        sys.exit(6)
    except GitError as error:
        errf('{} failed (return code: {})',
            ' '.join(error.cmd), error.returncode)
        if verbose >= ERR:
            sys.stderr.write(error.output + '\n')
        sys.exit(7)
    except InterruptException as error:
        infof('{}', error)
        sys.exit(0)


