#!/usr/bin/env python2

"""A wrapper script around arcanist's arc (part of the phabricator suite).

This wrapper allows a few options that arc does not, and also provides
a bit extra functionality.  Most of it is around 'arc diff' -- it
allows specifying reviewers on the commandline, and checking that
reviewer names are legal before putting up the form.  (A bit more
expensive, which is why phabricator doesn't do that by default
probably, but well worth it.)
"""

import argparse
import base64
import json
import os
import re
import subprocess
import sys
import time
import urllib
import urllib2


try:
    _DEFAULT_ARC_ROOT = os.path.dirname(os.path.dirname(__file__))
except NameError:     # __file__ not defined
    _DEFAULT_ARC_ROOT = '..'


def _update_arcrc(arc_root, default_arcrc_file='khan-bin/khan-arcrc',
                  user_arcrc_file='~/.arcrc', create_bak=True):
    """Update the user's .arcrc to include the contents of the standard one.

    This directory has khan-arcrc, which has the Khan-specific
    configuration options.  It can get updated from time to time.
    When this happens, we want to incorporate the updates into the
    user's .arcrc.  This routine does this.

    If the user's .arcrc has a section like
      'khan': {'do_not_auto_update': ['foo/bar', 'baz']}
    then this update process will skip the fields
    arcrc['foo']['bar'] and ['baz'] when updating the user's .arcrc.

    Arguments:
      arc_root: the root of the arcanist repository.
      default_arcrc_file: where the khan-system .arcrc file lives,
         relative to arc-root.
      user_arcrc_file: where the user's .arcrc file lives, relative to
         arc-root (so should usually be an absolute path).  Can include ~.
      create_bak: whether to copy the .arcrc to .arcrc.bak before updating
         its contents.
    """
    default_arcrc_file = os.path.join(arc_root, default_arcrc_file)
    default_arcrc = json.load(open(default_arcrc_file))

    user_arcrc_file = os.path.expanduser(user_arcrc_file)
    user_arcrc_file = os.path.join(arc_root, user_arcrc_file)
    try:
        user_arcrc = json.load(open(user_arcrc_file))
    except (IOError, OSError):
        user_arcrc = {}

    do_not_update = user_arcrc.get('khan', {}).get('do_not_auto_update', [])

    # Now update user_arcrc with the contents of default_arcrc.
    def _add_or_replace(from_root, to_root, key_path, do_not_update):
        """Recursively merges maps in from_root with to_root."""
        num_updates = 0
        for k in from_root:
            new_key_path = key_path + ('/' if key_path else '') + k
            if new_key_path in do_not_update:
                pass
            elif (isinstance(from_root.get(k), dict) and
                  isinstance(to_root.get(k), dict)):
                num_updates += _add_or_replace(from_root[k], to_root[k],
                                               new_key_path, do_not_update)
            elif to_root.get(k) != from_root.get(k):
                print >>sys.stderr, (
                    'Updating %s:%s to match the default value in %s.\n'
                    '  WAS: %s\n'
                    '  NOW: %s\n'
                    'If this was wrong, recover your old .arcrc from %s.bak\n'
                    'and add "%s" to %s:khan/do_not_auto_update.'
                    % (user_arcrc_file, new_key_path, default_arcrc_file,
                       to_root.get(k, '<empty>'), from_root.get(k, '<empty>'),
                       user_arcrc_file, new_key_path, user_arcrc_file))
                to_root[k] = from_root[k]
                num_updates += 1
        return num_updates

    num_updates = _add_or_replace(default_arcrc, user_arcrc, '', do_not_update)

    # As a migration step: if config.default is for an https url,
    # but we only have an http certificate, rename it to be an
    # https certificate instead.
    if user_arcrc.get('config', {}).get('default', '').startswith("https://"):
        hosts = user_arcrc.get('hosts', {})
        if hosts:
            http_cert = hosts.get('http://phabricator.khanacademy.org/api/')
            https_cert = hosts.get('https://phabricator.khanacademy.org/api/')
            if http_cert and not https_cert:   # http is canonical
                print >>sys.stderr, 'Copying your cert key from http to https'
                hosts['https://phabricator.khanacademy.org/api/'] = http_cert
                num_updates += 1

    # TODO(csilvers): delete the http cert, if the https cert exists, once
    #                 all .arcconfig's have moved to specifying the https
    #                 host for the conduit-uri.

    if num_updates:
        bak_filename = user_arcrc_file + '.bak'
        if create_bak and os.path.exists(user_arcrc_file):
            os.rename(user_arcrc_file, bak_filename)
        try:
            with open(user_arcrc_file, 'w') as f:
                json.dump(user_arcrc, f, indent=2, sort_keys=True)
            os.chmod(user_arcrc_file, 0o600)
        except:
            if os.path.exists(bak_filename):
                if os.path.exists(user_arcrc_file):  # in an inconsistent state
                    os.unlink(user_arcrc_file)
                os.rename(bak_filename, user_arcrc_file)
            raise


def _git_pull_in_dir(dir_to_pull):
    # If we are running as an exec step of a git rebase in another directory
    # (e.g. git rebase -x 'arc amend'), git sets GIT_DIR and GIT_WORK_TREE
    # which override the cwd as the location of the git tree to operate on.
    # But we definitely-no-really want to operate on the arcanist repo, so we
    # clear those out before calling git.
    env = os.environ.copy()
    env.pop('GIT_DIR', None)
    env.pop('GIT_WORK_TREE', None)
    try:
        subprocess.check_call(['git', 'pull', '-q'], cwd=dir_to_pull, env=env)
    except subprocess.CalledProcessError, why:
        sys.exit('While updating the arc binary, "git pull" failed in %s: %s.'
                 ' Make sure the repo is clean.' % (dir_to_pull, str(why)))


def _sync_arc(arc_root):
    """Make sure we're using the most up-to-date version of arc.

    Updates the arcanist repository.  Since this takes a
    few seconds, we only do it once a day.

    We also make sure the user's .arcrc file is up to date.

    Arguments:
      arc_root: the root of the arcanist repository.
    """
    try:
        # TODO(csilvers): use ~/.arc.pull or /tmp/arc.pull.<uid>
        last_pull_time = os.stat('/tmp/arc.pull').st_mtime
    except (IOError, OSError):
        last_pull_time = 0
    if last_pull_time + 24 * 60 * 60 >= time.time():
        return

    # TODO(csilvers): better to call 'arc upgrade' here instead.
    _git_pull_in_dir(arc_root)

    # Update the user's .arcrc with the default values (which may have
    # changed due to the pull).
    _update_arcrc(arc_root)

    # Also update our cache of usernames, for username completion with --rr.
    try:
        _get_user_info(arc_root, use_cache=False)   # updates the cache
    # TODO(csilvers): notice the problem in `_call_conduit` and raise
    # RuntimeError instead.
    except (RuntimeError, TypeError):    # probably means no ssh key
        pass

    open('/tmp/arc.pull', 'w').close()    # update the last-pull time.


def _conduit_call(arc_root, conduit_name, json_input={}):
    """Make an 'arc call-conduit' call with the given input."""
    arc = os.path.join(arc_root, 'bin', 'arc')
    p = subprocess.Popen([arc, 'call-conduit', '--', conduit_name],
                         stdout=subprocess.PIPE, stdin=subprocess.PIPE)
    conduit_stdout = p.communicate(input=json.dumps(json_input))[0]
    if p.returncode != 0:
        raise RuntimeError('Error getting %s info from phabricator: %s'
                           % (conduit_name, conduit_stdout))

    retval = json.loads(conduit_stdout)
    if retval.get('Error'):
        raise RuntimeError('Error getting %s info from phabricator: %s'
                           % (conduit_name, retval['errorMessage']))
    return retval['response']


class NotGitError(Exception):
    """Called git when not in a git repository."""


def _git_call(git_args):
    """Make a 'git' call and return the result.

    Raises NotGitError if run outside a git repo, or CalledProcessError
    if the git command fails for some other reason."""
    try:
        return subprocess.check_output(['git'] + git_args,
                                       stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError, why:
        if 'Not a git repository' in str(why):
            raise NotGitError()
        raise


def _try_save(filename, json_contents):
    try:
        with open(filename + '.tmp', 'w') as f:
            json.dump(json_contents, f)
        if os.path.exists(filename):
            os.unlink(filename)      # atomic rename
        os.rename(filename + '.tmp', filename)
    except (IOError, OSError):
        pass


def _get_user_info(arc_root, use_cache):
    """Retrieve a list of (username, realname) pairs of all phab. users."""
    if use_cache:
        try:
            # TODO(csilvers): use ~/.arc.usernames or /tmp/arc.usernames.<uid>
            with open('/tmp/arc.usernames') as f:
                return json.load(f)
        except (IOError, OSError, ValueError):
            pass

    user_info = _conduit_call(arc_root, 'user.query', {'limit': 1000000})
    retval = [(ui['userName'], ui['realName']) for ui in user_info
              if ('disabled' not in ui['roles'] and
                  'unverified' not in ui['roles'])]

    # Might as well update the cache while we've got all this data...
    _try_save('/tmp/arc.usernames', retval)
    return retval


def _get_project_info(arc_root, use_cache):
    """Retrieve a list of project_names of all active phabricator projects."""
    if use_cache:
        try:
            with open('/tmp/arc.projectnames') as f:
                return json.load(f)
        except (IOError, OSError, ValueError):
            pass

    project_info = _conduit_call(arc_root, 'project.query', {'limit': 1000000})
    retval = [pi['name'] for pi in project_info['data'].itervalues()
              if pi['color'] != 'disabled']

    _try_save('/tmp/arc.projectnames', retval)
    return retval


def _pick_a_number(prompt, max_number):
    """Prompt the user to pick a number from 1 to max_number, and return it."""
    try:
        pick = int(raw_input(prompt))
    except ValueError:
        raise ValueError('You must specify a number')
    if pick not in range(1, max_number + 1):
        raise IndexError('You must specify a number between 1 and %s'
                         % max_number)
    return pick


def normalize_usernames(arc_root, names, use_arc_caches=True):
    """Given a list of names or name substrings, return a list of usernames.

    The input names can be a phabricator username, or a (part of a)
    real name, or a substring of either.  It can also be a prefix of a
    phabricator project-name, including the leading '#'
    (e.g. '#infra').

    We normalize as follows.

    1) If the input name starts with @, or ends with @, we take it to
    be a username, and normalize to that ("alpert@" and "@alpert" both
    normalize to the user with username "alpert").

    2) If the input name is equal to one or more user's first-name,
    last-name, or username, those are the candidate users.  If there's
    only one candidate, we normalize to that user, otherwise we ask
    the client to pick the user.  If there are no candidates, we go to
    the next step.

    3) If the input name is a substring of "username (firstname
    lastname)" for one or more users, those are the candidate users.
    If there's only one candidate, we normalize to that user,
    otherwise we ask the client to pick the user.  If there are no
    candidates, raise a NameError.

    Arguments:
      arc_root: the root of the arcanist repository on the local filesystem.
      names: a list (or anything iterable) of strings.
      use_arc_caches: speed this up by using a cache.  Mostly for
          internal use.

    Returns:
      A list of phabricator usernames, in the same order as names.
      For a given name, the phabricator username is determined as
      described above.

    Raises:
      NameError: if an input name matches no name in the phabricator db.

    """
    # This code is adapted from github.com:Khan/kiln-review's review.py
    if not names:
        return []

    retval = []
    # Small optimization: if all args are @name/name@, no need for user-info.
    if not all(n.strip().endswith('@') or n.strip().startswith('@')
               for n in names):
        user_info = _get_user_info(arc_root, use_cache=use_arc_caches)
        # For ease of searching, we'll create a map from lowercase
        # username to username/realname, and also from lowercase realname
        # to username/realname.
        all_people = {}
        for (username, realname) in user_info:
            all_people[username.lower()] = (username, realname)
            all_people[realname.lower()] = (username, realname)

    # Likewise, without a # we don't need project-info.
    if any(n.strip().startswith('#') for n in names):
        project_info = _get_project_info(arc_root, use_cache=use_arc_caches)
        all_projects = {}
        for project in project_info:
            # Keep the same format as all_people to make the below easier.
            all_projects['#' + project.lower()] = ('#' + project, project)

    # For each asked-for reviewer, find the set of people records that
    # reviewer could be referring to.  Hopefully it's exactly one!
    for name in names:
        name = name.strip().lower()
        if name.endswith('@'):           # indicates an exact match desired
            retval.append(name[:-1])
            continue
        if name.startswith('@'):         # also indicates an exact match
            retval.append(name[1:])
            continue

        if name.startswith('#'):   # it's a project
            all_candidates = all_projects
            dup_msg = 'There are a few projects matching "%s"' % name
            untagged_name = name[1:]
        else:
            all_candidates = all_people
            dup_msg = 'There are a few folks matching "%s"' % name
            untagged_name = name

        # First try the 'word' match.
        candidate_reviewers = set()
        for (person, username_and_realname) in all_candidates.iteritems():
            if untagged_name in person.split():  # matches a word in 'person'
                candidate_reviewers.add(username_and_realname)

        # Then, if that doesn't work, move to 'substring' match.
        if not candidate_reviewers:
            for (person, username_and_realname) in all_candidates.iteritems():
                if untagged_name in person:      # is a substring of 'person'
                    candidate_reviewers.add(username_and_realname)

        candidate_reviewers = sorted(candidate_reviewers)

        if not candidate_reviewers:   # no person matched the reviewer
            if use_arc_caches:
                # Try again without the cache; maybe we're seeking a new user
                return normalize_usernames(arc_root, names,
                                           use_arc_caches=False)
            raise NameError('No reviewer found matching "%s"' % name)
        elif len(candidate_reviewers) > 1:
            print '\nHmm...%s:' % dup_msg
            for (i, username_and_realname) in enumerate(candidate_reviewers):
                print '  %s. %s (%s)' % (i + 1, username_and_realname[0],
                                         username_and_realname[1])
            pick = _pick_a_number('Which "%s" did you mean? ' % name,
                                  len(candidate_reviewers))
            retval.append(candidate_reviewers[pick - 1][0])
        else:
            retval.append(candidate_reviewers[0][0])

    return retval


def _extract_flag(arc_args, extract, comma_separate=False):
    """A mini getopt-style parser that leaves all other args alone.

    Like getopt.getopt, but ignores flag values it doesn't recognize.
    TODO(csilvers): use argparse.parse_known_args() instead:
       http://docs.python.org/2/library/argparse.html#partial-parsing

    Arguments:
        arc_args: the arg-list for arc: sys.argv[1:]
        extract: the flag to extract, e.g. '--rr'
        comma_separate: if True, split arg values on commas.  This
           causes us to return ['v1', 'v2', 'v3'] for each of these:
              --extract=v1,v2,v3
              --extract=v1 --extract=v2 --extract=v3
              --extract=v1,v2 --extract=v3

    Returns:
       A pair: (new_arc_args, extract_values).
       new_arc_args is the same as arc_args, but with extract and its
       argument removed.  If extract occurs more than once, all
       occurrences are removed.  extract_values are the arguments
       found for extract, as a list.  (The list will have 0 values if
       extract never occurs, 1 if it occurs once, 2 if it occurs
       twice, etc.)  Duplicates are removed from extract_values;
       otherwise, order is preserved.
    """
    new_arc_args = []
    extract_args = []
    i = 0
    while i < len(arc_args):
        if arc_args[i] == extract:
            extract_arg = arc_args[i + 1]
            i += 2
        elif arc_args[i].startswith(extract + '='):
            extract_arg = arc_args[i][len(extract + '='):]
            i += 1
        else:
            extract_arg = None
            new_arc_args.append(arc_args[i])
            i += 1

        if extract_arg is not None:
            if comma_separate:
                new_args = [a.strip() for a in extract_arg.split(',')
                            if a.strip()]
            else:
                new_args = [extract_arg]
            # We only add in the arg if it's not already in extract_args.
            # This removes dups while preserving relative ordering of
            # the arguments, in case that's meaningful to the author.
            for arg in new_args:
                if arg not in extract_args: # could be made more efficient...
                    extract_args.append(arg)

    return (new_arc_args, extract_args)


def normalize_rr_flags(arc_root, arc_args):
    """Replace --rr=<n1> --rr=<n2> with --reviewers <normalized n2>,...

    arc takes a flag --reviewers phabricator_username1,username2,...
    For historical reasons, and because it's easier to type, we allow
    --rr to specify reviewers.  We also allow --rr to be specified
    multiple times.  Finally, we allow the name in --rr to be a subset
    of a username -- or even of a real name -- and to prompt to clarify
    if the name is ambiguous.

    We accept '--rr=<n1>', '--rr=<n1> --rr=<n2>', '--rr=<n1>,<n2>', or
    even '--rr=<n1> --rr=<n2>,<n3>'.  In each case, the equals sign
    can be omitted -- that is, a space is ok instead of an equals sign.

    This flag takes care of all of this.  While --rr only makes sense
    for 'arc diff', we do this cleanup for all commands: it can't hurt
    (since --rr isn't a legal flag for any arcanist command) and that
    way we don't have to try to figure out if a command is an alias
    for diff.

    Arguments:
      arc_root: the root of the arcanist repository on the local filesystem.
      arc_args: the arguments pass to arc (argv[1+]).
      rr: what flag to search for: could be --cc, etc.

    Returns:
      A modified version of arc_args, with --rr flags replaces if any
      are present.  If none are present, arc_args is returned verbatim.
    """
    (retval, rr_args) = _extract_flag(arc_args, '--rr', comma_separate=True)

    if rr_args:
        # We need to convert our permissive reviewer args with phab usernames.
        reviewer_args = normalize_usernames(arc_root, rr_args)
        # It should always be ok to put --reviewers as the first arg to diff,
        # right after the 'diff' arg itself (which is arc_args[0])
        retval.insert(1, '--reviewers')
        retval.insert(2, ','.join(reviewer_args))

    return retval


def normalize_cc_flags(arc_root, arc_args):
    """Like normalize_rr_flags, but for --cc."""
    (retval, cc_args) = _extract_flag(arc_args, '--cc', comma_separate=True)
    if cc_args:
        # TODO(csilvers): support mailing lists too, when conduit adds that.
        reviewer_args = normalize_usernames(arc_root, cc_args)
        retval.insert(1, '--cc')
        retval.insert(2, ','.join(reviewer_args))
    return retval


def add_onto_for_arc_land(arc_args):
    """Figure out the 'parent' of this branch, and land onto that."""
    assert arc_args[0] == 'land'
    # If --onto is already in arc_args, don't add it
    for arg in arc_args:
        if arg.startswith('--onto'):
            return arc_args

    # This gives an error for if this branch isn't a tracking branch.
    # In that case, we'll require an explicit '--onto'.  cf.
    # http://stackoverflow.com/questions/171550/find-out-which-remote-branch-a-local-branch-is-tracking
    try:
        parent = _git_call(['rev-parse', '--abbrev-ref',
                            '--symbolic-full-name', '@{u}'])
    except NotGitError:
        # If we're an .hg repo or something, don't worry about --onto.
        return arc_args
    except subprocess.CalledProcessError:
        raise RuntimeError('You must explicitly specify --onto when landing'
                           ' a non-tracking branch (but better is to set'
                           ' branch:autosetupmerge in your .gitconfig!).')
    if '/' in parent:
        # If the parent is remote, then can't run 'arc land' at all; they
        # should run 'arc amend; git push' instead.
        raise RuntimeError("You can't call 'arc land' from a remote tracking"
                           " branch (you should call it from a local feature"
                           " branch).  Run 'arc amend' + 'git push' instead.")

    return arc_args + ['--onto', parent.strip()]


def maybe_munge_browse_arg(arc_args):
    """If the browse arg is a commit-ish, try to replace it with a review."""
    # 'arc browse' takes one argument, so it should be the last word.
    # If 'browse' isn't followed by an arg, assume it's HEAD.
    if arc_args[-1] == 'browse':
        arc_args.append('HEAD')

    browse_arg = arc_args[-1]

    # Check if the arg is a commit-ish, and if so get the log message.
    try:
        log_message = _git_call(['log', browse_arg])
    except (subprocess.CalledProcessError, NotGitError):
        return arc_args     # Not a commit-ish, not in a git repo, ...

    # If the commit-ish indicates a phabricator review, use it!
    m = re.search((r'^\s*Differential Revision: '
                   r'https?://phabricator.khanacademy.org/(.+)'),
                  log_message, re.MULTILINE)

    if not m and browse_arg == 'HEAD':
        # Use 'arc which' to see if the review is a parent of head.
        which_output = subprocess.check_output(['arc', 'which', '--'])
        m = re.search(r'^MATCHING REVISIONS.*?(D\d+)', which_output,
                      re.MULTILINE | re.DOTALL)

    if m:
        arc_args[-1] = m.group(1)
        print 'Loading %s (from %s)' % (arc_args[-1], browse_arg)

    return arc_args


def _format(s, indent=0):
    """Convert html to proper terminal markup, indent every line."""
    if sys.stdout.isatty():   # assume the tty is vt100
        m = {'<b>': '\033[1m', '</b>': '\033[0m',
             '<i>': '\033[4m', '</i>': '\033[0m'}
    else:
        m = {'<b>': '', '</b>': '', '<i>': '', '</i>': ''}

    for (markup, vt100_code) in m.iteritems():
        s = s.replace(markup, vt100_code)
    s = s.replace('\n', '\n' + ' ' * indent)
    return ' ' * indent + s


def print_full_help():
    """Print help plus Khan Academy-added commands on `arc help`."""
    # The `--` is just to avoid an infinite loop, since we interecpt arc help.
    subprocess.call(['arc', 'help', '--'])
    print
    print
    print _format('<b>KHAN ACADEMY COMMANDS</b>')
    print
    print _format('<b>znd</b> [options] <i>commit</i>', indent=6)
    print _format('Deploy the current branch to a znd', indent=6)
    print


_ZND_FLAGS = (
    ('version',
     ('The version-prefix to use for the znd; the same as VERSION at\n'
      'https://jenkins.khanacademy.org/job/deploy/job/deploy-znd/build\n'
      'The `znd-`, date, and username will be auto-prefixed.'),
     {}),

    ('--revision',
     ('The revision to use for the znd, overriding the default of HEAD.\n'
      'It can either be a git commit-ish (branchname or sha), or a\n'
      'phabricator revision like "D12345".'),
     {}),

    ('--services',
     ('Comma-separated list of services to deploy the znd to.'),
     {'default': 'static,dynamic'}),

    ('--skip-i18n',
     ('If specified, only deploy English files to the service.'),
     {'action': 'store_true'}),

    ('--modules',
     ('Comma-separated list of modules to deploy the znd to.\n'
      'Used when deploying to the `dynamic` service.'),
     {'default': 'default'}),

    ('--clean',
     ('How much to run `make clean` before deploying.  Legal values\n'
      'are "some", "most", "all", or "none".'),
     {'choices': ['some', 'most', 'all', 'none'], 'default': 'some'}),

    ('--prime',
     ('If specified, prime services after deploying them.\n'
      'This is only needed if many people will be accessing\n'
      'the deploy.'),
     {'action': 'store_true'}),

    ('--no-highmem-instance',
     ('If set, this version will be deployed on a regular\n'
      'instance class rather than the highmem instance class.\n'
      'Usually you want highmem to aid in filling caches and\n'
      'reducing timeouts.  If you will be sending significant\n'
      'traffic to your znd, consider setting this.'),
     {'action': 'store_true'}),

    ('--slack-channel',
     ('The slack channel to which to send failure alerts.\n'
      'Set to @yourusername to send a Slack DM (from Slackbot).'),
     {'default': '#1s-and-0s-deploys'}),
)


def print_znd_help():
    print _format("""\
<b>ZND WORKFLOW</b>

<b>znd</b> [options] <i>%s</i>

Deploy the current branch to a znd (non-default version)
in production.  This can deploy either a deploy branch
OR a review-branch!

%s
""" % (_ZND_FLAGS[0][0],
       _format('%s: %s' % (_ZND_FLAGS[0][0], _ZND_FLAGS[0][1]))))

    for (name, help, options) in _ZND_FLAGS[1:]:
        print _format('<i>%s</i>' % name, indent=6)
        print _format(help, indent=10)
        if 'default' in options:
            print _format('Default is: %s' % options['default'], indent=10)
        print


def send_znd(arc_args):
    try:
        arcrc = json.load(open(os.path.expanduser("~/.arcrc")))
    except (IOError, OSError):
        arcrc = {}
    auth_info = arcrc.get('khan', {}).get('jenkins_auth')
    if not auth_info:
        print """
ERROR: You must add a Jenkins API token to your ~/.arcrc to use '`arc znd`.
To do this, visit
    https://jenkins.khanacademy.org/user/<gmail username>@khanacademy.org/configure
and click on "Add New Token" under "API Token."  Name it `arc-znd`.
Then edit your ~/.arcrc and add this new entry under "khan":
    "jenkins_auth": {
      "username": "<gmail username>@khanacademy.org",
      "token": "<value of your new token>"
    }
Then re-run this `arc znd` command again.
"""
        sys.exit("(FATAL ERROR: Need to install jenkins auth.)")

    parser = argparse.ArgumentParser(prog='arc znd')
    for (name, help, options) in _ZND_FLAGS:
        options['help'] = help
        parser.add_argument(name, **options)

    assert arc_args[0] == 'znd'
    args = parser.parse_args(arc_args[1:])

    #
    # Figure out the commit-ish to deploy.  There are 4 possibilities:
    # 1) The commit-ish is specified directly via --revision.  We just
    #    use that.
    # 2) A phabricator diff is specified via --revision.  We convert
    #    it to a commit-ish (a git tag) and use that.
    # 3) HEAD exists on the remote.  In that case it's a commit-ish
    #    we can use.
    # 4) HEAD has a comment saying what the phabricator diff is.
    #    (This happens when it's the first commit in this diff.)
    #    We convert that to a commit-ish and use it.
    # It might seem that (4) is a subset of (3), but it's not, because
    # when you create a diff, arc updates HEAD locally to include the
    # the revision-id, but does not push that update to the remote.
    if args.revision:
        if args.revision.startswith('D'):
            commit = subprocess.check_output(
                ['revisionid-to-diffid.sh', '--tag', args.revision]
            ).strip()
        else:
            commit = args.revision
        commit_pretty_name = args.revision
    else:
        remote_tips = _git_call(['ls-remote', '-ht', 'origin'])
        head_sha = _git_call(['rev-parse', 'HEAD']).strip()
        # Technically, we can use head_sha if it satisfies the following:
        #    any(merge_base(head_sha, t) == head_sha for t in remote_tips)
        # But in practice people only want to deploy branches or tags, so
        # I don't bother to do all those merge-base calculations.
        m = re.search(r'^%s\s+refs/(?:tags|heads)/(.*)' % head_sha,
                      remote_tips, flags=re.MULTILINE)
        if m:
            commit = head_sha
            commit_pretty_name = m.group(1)
        else:
            log_output = subprocess.check_output(['git', 'log', '-n1'])
            m = re.search(r'^\s*Differential Revision: .*/(D\d+)',
                          log_output, flags=re.MULTILINE)
            if m:
                commit_pretty_name = m.group(1)
                commit = subprocess.check_output(
                    ['revisionid-to-diffid.sh', '--tag', commit_pretty_name]
                ).strip()
            else:
                raise ValueError(
                    "HEAD is not a deploy branch, and does not seem to be "
                    "pushed to phab either.  You must have run `arc diff` "
                    "on this commit.  (If you don't want it reviewed yet, "
                    "you can run `arc diff --draft`.)  You can also try "
                    "specifying --revision explicitly.")

    params = {
        'GIT_REVISION': commit,
        'VERSION': args.version,
    }
    params['SERVICES'] = args.services or ''
    params['SKIP_I18N'] = 'true' if args.skip_i18n else 'false'
    params['MODULES'] = args.modules or ''
    params['CLEAN'] = args.clean or ''
    params['PRIME'] = 'true' if args.prime else 'false'
    params['HIGHMEM_INSTANCE'] = (
        'false' if args.no_highmem_instance else 'true')
    params['SLACK_CHANNEL'] = args.slack_channel or ''

    print ('Deploying `%s` as a znd with parameters:\n%s'
           % (commit_pretty_name,
              '\n'.join('  %s: %s' % kv for kv in sorted(params.iteritems()))))
    x = raw_input('Continue? (y/N) ')
    if x.lower() != 'y':
        sys.exit("Aborted")

    auth = base64.b64encode('%(username)s:%(token)s' % auth_info)
    auth_header = {'Authorization': "Basic %s" % auth}
    url = ('https://jenkins.khanacademy.org/'
           '/crumbIssuer/api/xml?xpath=//crumb')
    req = urllib2.Request(url, headers=auth_header)
    r = urllib2.urlopen(req)
    crumb = r.read()

    url = ('https://jenkins.khanacademy.org/'
           '/job/deploy/job/deploy-znd/buildWithParameters')
    data = urllib.urlencode(params)
    headers = auth_header.copy()
    headers['Jenkins-Crumb'] = crumb
    req = urllib2.Request(url, data, headers=headers)
    urllib2.urlopen(req)

    print ("SUCCESS! Running at "
           "https://jenkins.khanacademy.org/job/deploy/job/deploy-znd/")


def _get_subcommand(arc_args):
    """Return the arc command being run given args to 'arc'.

    For instance, for 'arc --trace --config key=value diff --update D435'
    it would return 'diff'.
    """
    if not arc_args:
        return None

    # TODO(csilvers): support --trace, --config, etc.
    return arc_args[0]


def main(arc_args, arc_root=None):
    if arc_root is None:
        arc_root = _DEFAULT_ARC_ROOT

    arcanist_arc = os.path.join(arc_root, 'bin', 'arc')

    arc_subcommand = _get_subcommand(arc_args)

    try:
        _sync_arc(arc_root)
    except Exception as e:
        print >>sys.stderr, (
            "WARNING: Unable to update to the latest version of arc (in %s): "
            "%s. It's best to fix this since old `arc`s may stop working."
            % (_DEFAULT_ARC_ROOT, e))

    # If they are trying to install a certificate, skip our fancy
    # work; it won't succeed anyway without the certificate!  (Better
    # would be to grep for '"cert"' in ~/.arcrc, but I don't want to
    # have to deal with parsing --arcrc-file.
    if arc_subcommand == 'install-certificate':
        # This never returns.
        os.execv(arcanist_arc, [arcanist_arc] + arc_args)

    # Handle any --rr flags that may be in arc_args (--rr is a khan extension).
    # Likewise with --cc; while --cc is a standard arc flag, we're more
    # permissive in what we accept (real names, not just usernames).
    arc_args = normalize_rr_flags(arc_root, arc_args)
    arc_args = normalize_cc_flags(arc_root, arc_args)

    if arc_subcommand == 'land':
        arc_args = add_onto_for_arc_land(arc_args)

    if arc_subcommand == 'browse':
        arc_args = maybe_munge_browse_arg(arc_args)

    if arc_args == ['help']:
        print_full_help()
        return 0

    if arc_args == ['help', 'znd']:
        print_znd_help()
        return 0

    if arc_subcommand == 'znd':
        send_znd(arc_args)
        return 0

    # Now run the 'real' (arcanist) arc.
    os.execv(arcanist_arc, [arcanist_arc] + arc_args)


if __name__ == '__main__':
    main(sys.argv[1:])
