# ==================================================================================================
# Copyright 2011 Twitter, Inc.
# --------------------------------------------------------------------------------------------------
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this work except in compliance with the License.
# You may obtain a copy of the License in the LICENSE file, or at:
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==================================================================================================

from __future__ import print_function

import copy
import functools
import getpass
import hashlib
import logging
import os
import pkgutil
import shutil
import sys

from collections import defaultdict

from twitter.common.collections import OrderedDict, OrderedSet
from twitter.common.config import Properties
from twitter.common.dirutil import safe_open, safe_rmtree
from twitter.common.log.options import LogOptions

from twitter.pants import (
    get_buildroot,
    get_scm,
    is_codegen,
    is_exported as provides,
    is_internal,
    is_java,
    is_scala)
from twitter.pants.base import Address, Target
from twitter.pants.base.generator import Generator, TemplateData
from twitter.pants.ivy import Bootstrapper, Ivy
from twitter.pants.targets import (
    AnnotationProcessor,
    InternalTarget,
    JavaLibrary,
    JavaThriftLibrary,
    Resources,
    ScalaLibrary,
    ThriftJar,
    ThriftLibrary)
from twitter.pants.tasks.scm_publish import ScmPublish, Semver

from . import Task, TaskError


class PushDb(object):
  @staticmethod
  def load(path):
    """Loads a pushdb maintained in a properties file at the given path."""
    with open(path, 'r') as props:
      properties = Properties.load(props)
      return PushDb(properties)

  def __init__(self, props):
    self._props = props

  def as_jar_with_version(self, target):
    """
      Given an internal target, return a JarDependency with the last published revision filled in.
    """
    jar_dep, db_get, _ = self._accessors_for_target(target)

    major = int(db_get('revision.major', '0'))
    minor = int(db_get('revision.minor', '0'))
    patch = int(db_get('revision.patch', '0'))
    snapshot = db_get('revision.snapshot', 'false').lower() == 'true'
    sha = db_get('revision.sha', None)
    fingerprint = db_get('revision.fingerprint', None)
    semver = Semver(major, minor, patch, snapshot=snapshot)
    jar_dep.rev = semver.version()
    return jar_dep, semver, sha, fingerprint

  def set_version(self, target, version, sha, fingerprint):
    version = version if isinstance(version, Semver) else Semver.parse(version)
    _, _, db_set = self._accessors_for_target(target)
    db_set('revision.major', version.major)
    db_set('revision.minor', version.minor)
    db_set('revision.patch', version.patch)
    db_set('revision.snapshot', str(version.snapshot).lower())
    db_set('revision.sha', sha)
    db_set('revision.fingerprint', fingerprint)

  def _accessors_for_target(self, target):
    jar_dep, _, exported = target.get_artifact_info()
    if not exported:
      raise ValueError

    def key(prefix):
      return '%s.%s%%%s' % (prefix, jar_dep.org, jar_dep.name)

    def getter(prefix, default=None):
      return self._props.get(key(prefix), default)

    def setter(prefix, value):
      self._props[key(prefix)] = value

    return jar_dep, getter, setter

  def dump(self, path):
    """Saves the pushdb as a properties file to the given path."""
    with open(path, 'w') as props:
      Properties.dump(self._props, props)


class DependencyWriter(object):
  """
    Builds up a template data representing a target and applies this to a template to produce a
    dependency descriptor.
  """

  def __init__(self, get_db, template_relpath):
    self.get_db = get_db
    self.template_relpath = template_relpath

  def write(self, target, path, confs=None, synth=False):
    def as_jar(internal_target, is_tgt=False):
      jar, _, _, _ = self.get_db(internal_target).as_jar_with_version(internal_target)
      if synth and is_tgt:
        jar.name += '-only'
      return jar

    # TODO(John Sirois): a dict is used here to de-dup codegen targets which have both the original
    # codegen target - say java_thrift_library - and the synthetic generated target (java_library)
    # Consider reworking codegen tasks to add removal of the original codegen targets when rewriting
    # the graph
    dependencies = OrderedDict()
    internal_codegen = {}
    for dep in target_internal_dependencies(target):
      jar = as_jar(dep)
      dependencies[(jar.org, jar.name)] = self.internaldep(jar, dep, synth)
      if is_codegen(dep):
        internal_codegen[jar.name] = jar.name
    for jar in target.jar_dependencies:
      if jar.rev:
        classifier = jar.classifier if isinstance(jar, ThriftJar) else None
        dependencies[(jar.org, jar.name)] = self.jardep(jar, classifier=classifier)
    target_jar = self.internaldep(as_jar(target, is_tgt=True)).extend(
      dependencies=dependencies.values()
    )

    template_kwargs = self.templateargs(target_jar, confs, synth)
    with safe_open(path, 'w') as output:
      template = pkgutil.get_data(__name__, self.template_relpath)
      Generator(template, **template_kwargs).write(output)

  def templateargs(self, target_jar, confs=None, synth=False):
    """
      Subclasses must return a dict for use by their template given the target jar template data
      and optional specific ivy configurations.
    """
    raise NotImplementedError()

  def internaldep(self, jar_dependency, dep=None, synth=False):
    """
      Subclasses must return a template data for the given internal target (provided in jar
      dependency form).
    """
    raise NotImplementedError()

  def jardep(self, jar_dependency, classifier=None):
    """Subclasses must return a template data for the given external jar dependency."""
    raise NotImplementedError()

  def create_exclude(self, exclude):
    return TemplateData(org=exclude.org, name=exclude.name)


class PomWriter(DependencyWriter):
  def __init__(self, get_db):
    super(PomWriter, self).__init__(get_db, os.path.join('templates', 'jar_publish', 'pom.mk'))

  def templateargs(self, target_jar, confs=None, synth=False):
    return dict(artifact=target_jar)

  def jardep(self, jar, classifier=None, synth=False):
    return TemplateData(
      org=jar.org,
      name=jar.name + ('-only' if synth else ''),
      rev=jar.rev,
      scope='compile',
      classifier=(classifier if classifier is not None else jar.classifier),
      excludes=[self.create_exclude(exclude) for exclude in jar.excludes if exclude.name]
    )

  def internaldep(self, jar_dependency, dep=None, synth=False):
    classifier = 'idl' if (is_codegen(dep) and synth) or isinstance(dep, ThriftLibrary) else None
    return self.jardep(jar_dependency, classifier=classifier, synth=synth)


class IvyWriter(DependencyWriter):
  def __init__(self, get_db):
    super(IvyWriter, self).__init__(get_db, os.path.join('templates', 'ivy_resolve', 'ivy.mk'))

  def templateargs(self, target_jar, confs=None, synth=False):
    return dict(lib=target_jar.extend(
      is_idl=synth,
      publications=set(confs) if confs else set(),
      overrides=None
    ))

  def _jardep(self, jar, transitive=True, configurations='default', classifier=None, synth=False):
    return TemplateData(
      org=jar.org,
      module=jar.name + ('-only' if synth else ''),
      version=jar.rev,
      mutable=False,
      force=jar.force,
      excludes=[self.create_exclude(exclude) for exclude in jar.excludes],
      transitive=transitive,
      artifacts=jar.artifacts,
      is_idl=(classifier == 'idl'),
      configurations=configurations,
    )

  def jardep(self, jar, classifier=None):
    return self._jardep(jar,
      transitive=jar.transitive,
      configurations=';'.join(jar._configurations),
      classifier=classifier
    )

  def internaldep(self, jar_dependency, dep=None, synth=False):
    classifier = 'idl' if (is_codegen(dep) and synth) or isinstance(dep, ThriftLibrary) else None
    return self._jardep(jar_dependency, classifier=classifier, synth=synth)


def is_exported(target):
  return provides(target) and isinstance(target, (AnnotationProcessor, JavaLibrary, ScalaLibrary,
                                                  ThriftLibrary))


def coordinate(org, name, rev=None):
  return '%s#%s;%s' % (org, name, rev) if rev else '%s#%s' % (org, name)


def jar_coordinate(jar, rev=None):
  return coordinate(jar.org, jar.name, rev or jar.rev)


def target_internal_dependencies(target):
  return filter(lambda tgt: not isinstance(tgt, Resources), target.internal_dependencies)


class JarPublish(ScmPublish, Task):
  """Publish jars to a maven repository.

  At a high-level, pants uses `Apache Ivy <http://ant.apache.org/ivy/>`_ to
  publish artifacts to Maven-style repositories. Pants performs prerequisite
  tasks like compiling, creating jars, and generating ``pom.xml`` files then
  invokes Ivy to actually publish the artifacts, so publishing is largely
  configured in ``ivysettings.xml``. ``BUILD`` and ``pants.ini`` files
  primarily provide linkage between publishable targets and the
  Ivy ``resolvers`` used to publish them.

  The following target types are publishable: :ref:`bdict_java_library`,
  :ref:`bdict_scala_library`, :ref:`bdict_thrift_library`,
  :ref:`bdict_annotation_processor`.
  Targets to publish and their dependencies must be publishable target
  types and specify the ``provides`` argument. One exception is
  :ref:`bdict_jar`\s - pants will generate a pom file that
  depends on the already-published jar.

  Example usage: ::

     # By default pants will perform a dry-run.
     ./pants goal clean-all publish src/java/com/twitter/mybird

     # Actually publish.
     ./pants goal clean-all publish src/java/com/twitter/mybird --no-publish-dryrun

  Please see ``./pants goal publish -h`` for a detailed description of all
  publishing options.

  Publishing can be configured in ``pants.ini`` as follows.

  ``jar-publish`` section:

  * ``repos`` - Required dictionary of settings for repos that may be pushed to.
  * ``ivy_jvmargs`` - Optional list of JVM command-line args when invoking Ivy.
  * ``restrict_push_branches`` - Optional list of branches to restrict publishing to.

  Example pants.ini jar-publish repos dictionary: ::

     repos = {
       # repository target name is paired with this key
       'myrepo': {
         # ivysettings.xml resolver to use for publishing
         'resolver': 'maven.twttr.com',
         # ivy configurations to publish
         'confs': ['default', 'sources', 'docs'],
         # address of a Credentials target to use when publishing
         'auth': 'address/of/credentials/BUILD:target',
         # help message if unable to initialize the Credentials target.
         'help': 'Please check your credentials and try again.',
       },
     }

  Additionally the ``ivy`` section ``ivy_settings`` property specifies which
  Ivy settings file to use when publishing is required.
  """

  _CONFIG_SECTION = 'jar-publish'

  @classmethod
  def setup_parser(cls, option_group, args, mkflag):
    # TODO(John Sirois): Support a preview mode that outputs a file with entries like:
    # artifact id:
    # revision:
    # publish: (true|false)
    # changelog:
    #
    # Allow re-running this goal with the file as input to support forcing an arbitrary set of
    # revisions and supply of hand endited changelogs.

    option_group.add_option(mkflag("dryrun"), mkflag("dryrun", negate=True),
                            dest="jar_publish_dryrun", default=True,
                            action="callback", callback=mkflag.set_bool,
                            help="[%default] Runs through a push without actually pushing "
                                 "artifacts, editing publish dbs or otherwise writing data")

    option_group.add_option(mkflag("commit", negate=True),
                            dest="jar_publish_commit", default=True,
                            action="callback", callback=mkflag.set_bool,
                            help="Turns off commits of the push db for local testing.")

    local_flag = mkflag("local")
    option_group.add_option(local_flag, dest="jar_publish_local",
                            help="Publishes jars to a maven repository on the local filesystem at "
                                 "the specified path.")

    option_group.add_option(mkflag("local-snapshot"), mkflag("local-snapshot", negate=True),
                            dest="jar_publish_local_snapshot", default=True,
                            action="callback", callback=mkflag.set_bool,
                            help="[%%default] If %s is specified, publishes jars with '-SNAPSHOT' "
                                 "revisions." % local_flag)

    option_group.add_option(mkflag("transitive"), mkflag("transitive", negate=True),
                            dest="jar_publish_transitive", default=True,
                            action="callback", callback=mkflag.set_bool,
                            help="[%default] Publishes the specified targets and all their "
                                 "internal dependencies transitively.")

    option_group.add_option(mkflag("force"), mkflag("force", negate=True),
                            dest="jar_publish_force", default=False,
                            action="callback", callback=mkflag.set_bool,
                            help="[%default] Forces pushing jars even if there have been no "
                                 "changes since the last push.")

    flag = mkflag('override')
    option_group.add_option(flag, action='append', dest='jar_publish_override',
                            help='''Specifies a published jar revision override in the form:
                            ([org]#[name]|[target spec])=[new revision]

                            For example, to specify 2 overrides:
                            %(flag)s=com.twitter.common#quantity=0.1.2 \\
                            %(flag)s=src/java/com/twitter/common/base=1.0.0 \\
                            ''' % dict(flag=flag))

    flag = mkflag("restart-at")
    option_group.add_option(flag, dest="jar_publish_restart_at",
                            help='''Restart a fail push at the given jar.  Jars can be identified by
                            maven coordinate [org]#[name] or target.

                            For example:
                            %(flag)s=com.twitter.common#quantity

                            Or:
                            %(flag)s=src/java/com/twitter/common/base
                            ''' % dict(flag=flag))

  def __init__(self, context, scm=None):
    Task.__init__(self, context)
    ScmPublish.__init__(self, scm or get_scm(),
                        self.context.config.getlist(
                          JarPublish._CONFIG_SECTION, 'restrict_push_branches'))
    self.outdir = os.path.join(context.config.getdefault('pants_workdir'), 'publish')
    self.cachedir = os.path.join(self.outdir, 'cache')

    self._jvmargs = context.config.getlist(JarPublish._CONFIG_SECTION, 'ivy_jvmargs', default=[])

    if context.options.jar_publish_local:
      local_repo = dict(
        resolver='publish_local',
        path=os.path.abspath(os.path.expanduser(context.options.jar_publish_local)),
        confs=['*'],
        auth=None
      )
      self.repos = defaultdict(lambda: local_repo)
      self.commit = False
      self.snapshot = context.options.jar_publish_local_snapshot
    else:
      self.repos = context.config.getdict(JarPublish._CONFIG_SECTION, 'repos')
      for repo, data in self.repos.items():
        auth = data.get('auth')
        if auth:
          credentials = context.resolve(auth).next()
          user = credentials.username(data['resolver'])
          password = credentials.password(data['resolver'])
          self.context.log.debug('Found auth for repo=%s user=%s' % (repo, user))
          self.repos[repo]['username'] = user
          self.repos[repo]['password'] = password
      self.commit = context.options.jar_publish_commit
      self.snapshot = False

    self.ivycp = context.config.getlist('ivy', 'classpath')
    self.ivysettings = context.config.get('jar-publish', 'ivy_settings')

    self.dryrun = context.options.jar_publish_dryrun
    self.transitive = context.options.jar_publish_transitive
    self.force = context.options.jar_publish_force

    def parse_jarcoordinate(coordinate):
      components = coordinate.split('#', 1)
      if len(components) == 2:
        org, name = components
        return org, name
      else:
        try:
          address = Address.parse(get_buildroot(), coordinate)
          try:
            target = Target.get(address)
            if not target:
              siblings = Target.get_all_addresses(address.buildfile)
              prompt = 'did you mean' if len(siblings) == 1 else 'maybe you meant one of these'
              raise TaskError('%s => %s?:\n    %s' % (address, prompt,
                                                      '\n    '.join(str(a) for a in siblings)))
            if not is_exported(target):
              raise TaskError('%s is not an exported target' % coordinate)
            return target.provides.org, target.provides.name
          except (ImportError, SyntaxError, TypeError):
            raise TaskError('Failed to parse %s' % address.buildfile.relpath)
        except IOError:
          raise TaskError('No BUILD file could be found at %s' % coordinate)

    self.overrides = {}
    if context.options.jar_publish_override:
      def parse_override(override):
        try:
          coordinate, rev = override.split('=', 1)
          try:
            rev = Semver.parse(rev)
          except ValueError as e:
            raise TaskError('Invalid version %s: %s' % (rev, e))
          return parse_jarcoordinate(coordinate), rev
        except ValueError:
          raise TaskError('Invalid override: %s' % override)

      self.overrides.update(parse_override(o) for o in context.options.jar_publish_override)

    self.restart_at = None
    if context.options.jar_publish_restart_at:
      self.restart_at = parse_jarcoordinate(context.options.jar_publish_restart_at)

    context.products.require('jars')
    context.products.require('source_jars')
    context.products.require('idl_jars')

  def execute(self, targets):
    self.check_clean_master(commit=(not self.dryrun and self.commit))

    exported_targets = self.exported_targets()
    self.check_targets(exported_targets)

    pushdbs = {}

    def get_db(tgt):
      # TODO(tdesai) Handle resource type in get_db.
      if tgt.provides is None:
        raise TaskError('trying to publish target %r which does not provide an artifact' % tgt)
      dbfile = tgt.provides.repo.push_db
      result = pushdbs.get(dbfile)
      if not result:
        db = PushDb.load(dbfile)
        repo = self.repos[tgt.provides.repo.name]
        result = (db, dbfile, repo)
        pushdbs[dbfile] = result
      return result

    def get_pushdb(tgt):
      return get_db(tgt)[0]

    def fingerprint_internal(tgt):
      if not is_internal(tgt):
        raise ValueError('Expected an internal target for fingerprinting, got %s' % tgt)
      pushdb, _, _ = get_db(tgt)
      _, _, _, fingerprint = pushdb.as_jar_with_version(tgt)
      return fingerprint or '0.0.0'

    def lookup_synthetic_target(tgt):
      # TODO(phom) this only works for Thrift Library, not Protobuf
      return tgt.derived_from if isinstance(tgt.derived_from, JavaThriftLibrary) else None

    def artifact_path(jar, version, name=None, suffix='', extension='jar', artifact_ext=''):
      return os.path.join(self.outdir, jar.org, jar.name + artifact_ext,
                          '%s%s-%s%s.%s' % ((name or jar.name),
                                            artifact_ext if name != 'ivy' else '',
                                            version,
                                            suffix,
                                            extension))

    def stage_artifact(tgt, jar, version, changelog, confs=None, artifact_ext='', synth=False):
      def path(name=None, suffix='', extension='jar'):
        return artifact_path(jar, version, name=name, suffix=suffix, extension=extension,
                             artifact_ext=artifact_ext)

      with safe_open(path(suffix='-CHANGELOG', extension='txt'), 'w') as changelog_file:
        changelog_file.write(changelog)
      ivyxml = path(name='ivy', extension='xml')

      IvyWriter(get_pushdb).write(tgt, ivyxml, confs=confs, synth=synth)
      PomWriter(get_pushdb).write(tgt, path(extension='pom'), synth=synth)

      return ivyxml

    def copy_artifact(tgt, version, typename, suffix='', artifact_ext=''):
      genmap = self.context.products.get(typename)
      for basedir, jars in genmap.get(tgt).items():
        for artifact in jars:
          path = artifact_path(jar, version, suffix=suffix, artifact_ext=artifact_ext)
          shutil.copy(os.path.join(basedir, artifact), path)

    def stage_artifacts(tgt, jar, version, changelog, confs=None, synth_target=None):
      is_idl = isinstance(tgt, ThriftLibrary)
      class_target = None if is_idl else tgt
      idl_target = synth_target or (tgt if is_idl else None)

      class_ivyxml_path = idl_ivyxml_path = None
      if class_target:
        class_ivyxml_path = stage_artifact(tgt, jar, version, changelog, confs)
        copy_artifact(tgt, version, typename='jars')
        copy_artifact(tgt, version, typename='source_jars', suffix='-sources')

        jarmap = self.context.products.get('javadoc_jars')
        if not jarmap.empty() and (is_java(tgt) or is_scala(tgt)):
          copy_artifact(tgt, version, typename='javadoc_jars', suffix='-javadoc')

      if idl_target:
        synth = bool(synth_target)
        artifact_ext = '-only' if synth else ''
        idl_ivyxml_path = stage_artifact(idl_target, jar, version, changelog, confs=['idl'],
                                         artifact_ext=artifact_ext, synth=synth)
        copy_artifact(idl_target, version, typename='idl_jars', suffix='-idl',
                      artifact_ext=artifact_ext)

      return class_ivyxml_path, idl_ivyxml_path

    if self.overrides:
      print('Publishing with revision overrides:\n  %s' % '\n  '.join(
        '%s=%s' % (coordinate(org, name), rev) for (org, name), rev in self.overrides.items()
      ))

    head_sha = self.scm.commit_id

    safe_rmtree(self.outdir)
    published = []
    skip = (self.restart_at is not None)
    for target in exported_targets:
      synth_target = lookup_synthetic_target(target)
      pushdb, dbfile, repo = get_db(target)
      jar, semver, sha, fingerprint = pushdb.as_jar_with_version(target)

      if synth_target:
        # add idl artifact to the published cache
        tmp_jar = copy.copy(jar)
        tmp_jar.name += '-only'
        published.append(tmp_jar)
      published.append(jar)

      if skip and (jar.org, jar.name) == self.restart_at:
        skip = False

      newver = self.overrides.get((jar.org, jar.name)) or semver.bump()
      if self.snapshot:
        newver = newver.make_snapshot()

      if newver <= semver:
        raise TaskError('Requested version %s must be greater than the current version %s' % (
            newver.version(), semver.version()
            ))

      newfingerprint = self.fingerprint(target, fingerprint_internal)
      no_changes = newfingerprint == fingerprint

      if no_changes:
        changelog = 'No changes for %s - forced push.\n' % jar_coordinate(jar, semver.version())
      else:
        changelog = self.changelog(target, sha) or 'Direct dependencies changed.\n'

      if no_changes and not self.force:
        print('No changes for %s' % jar_coordinate(jar, semver.version()))
        stage_artifacts(target, jar, (newver if self.force else semver).version(), changelog,
                        synth_target=synth_target)
      elif skip:
        print('Skipping %s to resume at %s' % (
          jar_coordinate(jar, (newver if self.force else semver).version()),
          coordinate(self.restart_at[0], self.restart_at[1])
        ))
        stage_artifacts(target, jar, semver.version(), changelog, synth_target=synth_target)
      else:
        if not self.dryrun:
          # Confirm push looks good
          if no_changes:
            print(changelog)
          else:
            print('\nChanges for %s since %s @ %s:\n\n%s' % (
              coordinate(jar.org, jar.name), semver.version(), sha, changelog
            ))
          if os.isatty(sys.stdin.fileno()):
            push = raw_input('Publish %s with revision %s ? [y|N] ' % (
              coordinate(jar.org, jar.name), newver.version()
            ))
            print('\n')
            if push.strip().lower() != 'y':
              raise TaskError('User aborted push')

        pushdb.set_version(target, newver, head_sha, newfingerprint)

        ivyxml, idl_ivyxml = stage_artifacts(target, jar, newver.version(), changelog,
                                             confs=repo['confs'], synth_target=synth_target)

        if self.dryrun:
          print('Skipping publish of %s in test mode.' % jar_coordinate(jar, newver.version()))
        else:
          resolver = repo['resolver']
          path = repo.get('path')

          # Get authentication for the publish repo if needed
          jvm_args = self._jvmargs
          if repo.get('auth'):
            user = repo.get('username')
            password = repo.get('password')
            if user and password:
              jvm_args.append('-Dlogin=%s' % user)
              jvm_args.append('-Dpassword=%s' % password)
            else:
              raise TaskError('Unable to publish to %s. %s' %
                              (repo['resolver'], repo.get('help', '')))

          # Do the publish
          def publish(ivyxml_path):
            ivysettings = self.generate_ivysettings(published, publish_local=path)
            args = [
              '-settings', ivysettings,
              '-ivy', ivyxml_path,
              '-deliverto', '%s/[organisation]/[module]/ivy-[revision].xml' % self.outdir,
              '-publish', resolver,
              '-publishpattern', '%s/[organisation]/[module]/'
                                 '[artifact]-[revision](-[classifier]).[ext]' % self.outdir,
              '-revision', newver.version(),
              '-m2compatible',
            ]

            if LogOptions.stderr_log_level() == logging.DEBUG:
              args.append('-verbose')

            if self.snapshot:
              args.append('-overwrite')

            try:
              ivy = Bootstrapper.default_ivy()
              ivy.execute(args, jvm_args=jvm_args)
            except (Bootstrapper.Error, Ivy.Error) as e:
              raise TaskError('Failed to push %s! %s' % (jar_coordinate(jar, newver.version()), e))

          if ivyxml:
            publish(ivyxml)
          if idl_ivyxml:
            publish(idl_ivyxml)

          if self.commit:
            org = jar.org
            name = jar.name
            rev = newver.version()
            args = dict(
              org=org,
              name=name,
              rev=rev,
              coordinate=coordinate(org, name, rev),
              user=getpass.getuser(),
              cause='with forced revision' if (org, name) in self.overrides else '(autoinc)'
            )

            pushdb.dump(dbfile)
            self.commit_push(coordinate(org, name, rev))
            self.scm.refresh()
            self.scm.tag('%(org)s-%(name)s-%(rev)s' % args,
                         message='Publish of %(coordinate)s initiated by %(user)s %(cause)s' % args)

  def check_targets(self, targets):
    invalid = defaultdict(set)

    def collect(publish_target, walked_target):
      if hasattr(walked_target, "sources") and not walked_target.sources:
        invalid[publish_target].add((walked_target, 'No sources.'))
      if hasattr(walked_target, "provides") and not walked_target.provides:
        invalid[publish_target].add((walked_target, 'Does not provide an artifact.'))

    for target in targets:
      target.walk(functools.partial(collect, target))

    if invalid:
      msg = list()
      for target, reasons in sorted(invalid.items(), reverse=True):
        msg.append('\n  Cannot publish %s due to:' % target.address)
        for invalid_target, reason in sorted(reasons, reverse=True):
          msg.append('\n    %s - %s' % (invalid_target.address, reason))

      raise TaskError('The following errors must be resolved to publish.%s' % ''.join(msg))

  def exported_targets(self):
    candidates = set()
    if self.transitive:
      candidates.update(self.context.targets())
    else:
      candidates.update(self.context.target_roots)

      def get_synthetic(lang, target):
        mappings = self.context.products.get(lang).get(target)
        if mappings:
          for generated in mappings.itervalues():
            for synthetic in generated:
              yield synthetic

      # Handle the case where a code gen target is in the listed roots and the thus the publishable
      # target is a synthetic twin generated by a code gen task upstream.
      for candidate in self.context.target_roots:
        candidates.update(get_synthetic('java', candidate))
        candidates.update(get_synthetic('scala', candidate))

    def exportable(tgt):
      return tgt in candidates and is_exported(tgt)

    return OrderedSet(filter(exportable,
                             reversed(InternalTarget.sort_targets(filter(exportable, candidates)))))

  def fingerprint(self, target, fingerprint_internal):
    sha = hashlib.sha1()

    for source in sorted(target.sources):
      path = os.path.join(target.target_base, source)
      with open(path) as fd:
        sha.update(source)
        sha.update(fd.read())

    # TODO(John Sirois): handle resources and circular dep scala_library java_sources

    for jarsig in sorted([jar_coordinate(j) for j in target.jar_dependencies if j.rev]):
      sha.update(jarsig)

    # TODO(tdesai) Handle resource type in get_db.
    internal_dependencies = sorted(target_internal_dependencies(target), key=lambda t: t.id)
    for internal_target in internal_dependencies:
      fingerprint = fingerprint_internal(internal_target)
      sha.update(fingerprint)

    return sha.hexdigest()

  def changelog(self, target, sha):
    return self.scm.changelog(from_commit=sha,
                              files=[os.path.join(target.target_base, source)
                                     for source in target.sources])

  def generate_ivysettings(self, publishedjars, publish_local=None):
    template_relpath = os.path.join('templates', 'jar_publish', 'ivysettings.mk')
    template = pkgutil.get_data(__name__, template_relpath)
    with safe_open(os.path.join(self.outdir, 'ivysettings.xml'), 'w') as wrapper:
      generator = Generator(template,
                            ivysettings=self.ivysettings,
                            dir=self.outdir,
                            cachedir=self.cachedir,
                            published=[TemplateData(org=jar.org, name=jar.name)
                                       for jar in publishedjars],
                            publish_local=publish_local)
      generator.write(wrapper)
      return wrapper.name
