lib/jeweler/generator.rb



require 'git'
require 'erb'

require 'net/http'
require 'uri'

class Jeweler
  class NoGitUserName < StandardError
  end
  class NoGitUserEmail < StandardError
  end
  class FileInTheWay < StandardError
  end
  class NoGitHubRepoNameGiven < StandardError
  end
  class NoGitHubUser < StandardError
  end
  class NoGitHubToken < StandardError
  end
  class GitInitFailed < StandardError
  end    

  class Generator    
    attr_accessor :target_dir, :user_name, :user_email, :summary, :testing_framework,
                  :github_repo_name, :github_username, :github_token,
                  :repo, :should_create_repo, :should_use_cucumber, :should_setup_rubyforge

    SUPPORTED_TESTING_FRAMEWORKS = [:shoulda, :testunit, :bacon, :rspec, :micronaut, :minitest]

    def initialize(github_repo_name, options = {})
      if github_repo_name.nil? || github_repo_name.squeeze.strip == ""
        raise NoGitHubRepoNameGiven
      end

      self.github_repo_name   = github_repo_name

      self.testing_framework  = (options[:testing_framework] || :shoulda).to_sym
      unless SUPPORTED_TESTING_FRAMEWORKS.include? self.testing_framework
        raise ArgumentError, "Unsupported testing framework (#{testing_framework})"
      end

      self.target_dir         = options[:directory] || self.github_repo_name

      self.should_create_repo = options[:create_repo]
      self.summary            = options[:summary] || 'TODO'
      self.should_use_cucumber= options[:use_cucumber]
      self.should_setup_rubyforge = options[:rubyforge]

      use_user_git_config
      
    end

    def run
      create_files
      gitify
      $stdout.puts "Jeweler has prepared your gem in #{github_repo_name}"
      if should_create_repo
        create_and_push_repo
        $stdout.puts "Jeweler has pushed your repo to #{github_url}"
        enable_gem_for_repo
        $stdout.puts "Jeweler has enabled gem building for your repo"
      end
    end

    # Directory where 'tests' live
    def test_dir
      test_or_spec
    end

    # Default rake task to use
    def default_task
      case testing_framework.to_sym
      when :shoulda, :testunit, :minitest
        'test'
      when :bacon, :rspec
        'spec'
      when :micronaut
        'examples'
      else
        raise ArgumentError, "Don't know default task for #{testing_framework}"
      end
    end

    def feature_support_require
      case testing_framework.to_sym
      when :testunit, :shoulda, :bacon # NOTE bacon doesn't really work inside of cucumber
        'test/unit/assertions'
      when :minitest
        'mini/test'
      when :rspec
        'spec/expectations'
      when :micronaut
        'micronaut/expectations'
      else
        raise "Don't know what to require for #{testing_framework}"
      end
    end

    def feature_support_extend
      case testing_framework.to_sym
      when :testunit, :shoulda, :bacon # NOTE bacon doesn't really work inside of cucumber
        'Test::Unit::Assertions'
      when :minitest
        'Mini::Test::Assertions'
      when :rspec
        nil
      when :micronaut
        'Micronaut::Matchers'
      else
        raise "Don't know what to extend for #{testing_framework}"
      end
    end

    def github_remote
      "git@github.com:#{github_username}/#{github_repo_name}.git"
    end

    def github_url
      "http://github.com/#{github_username}/#{github_repo_name}"
    end

    
    def constant_name
      self.github_repo_name.split(/[-_]/).collect{|each| each.capitalize }.join
    end

    def file_name_prefix
      self.github_repo_name.gsub('-', '_')
    end

    def lib_dir
      'lib'
    end

    def test_dir
      case testing_framework.to_sym
      when :shoulda, :testunit, :minitest
        'test'
      when :bacon, :rspec
        'spec'
      when :micronaut
        'examples'
      else
        raise ArgumentError, "Don't know test dir for #{testing_framework.inspect}"
      end
    end

    def test_filename
      "#{file_name_prefix}_#{test_or_spec}.rb"
    end

    def test_helper_filename
      "#{test_or_spec}_helper.rb"
    end

    def feature_filename
      "#{file_name_prefix}.feature"
    end

    def steps_filename
      "#{file_name_prefix}_steps.rb"
    end

    def features_dir
      'features'
    end

    def features_support_dir
      File.join(features_dir, 'support')
    end

    def features_steps_dir
      File.join(features_dir, 'step_definitions')
    end

    def test_or_spec
      case testing_framework.to_sym
      when :shoulda, :testunit, :minitest
        'test'
      when :bacon, :rspec
        'spec'
      when :micronaut
        'example'
      else
        raise ArgumentError, "Unknown test style: #{testing_framework}"
      end
    end


  protected

    # This is in a separate method so we can stub it out during testing
    def read_git_config
      # we could just use Git::Base's .config, but that relies on a repo being around already
      # ... which we don't have yet, since this is part of a sanity check
      lib = Git::Lib.new(nil, nil)
      config = lib.parse_config '~/.gitconfig'
    end

  private
    def create_files
      unless File.exists?(target_dir) || File.directory?(target_dir)
        FileUtils.mkdir target_dir
      else
        raise FileInTheWay, "The directory #{target_dir} already exists, aborting. Maybe move it out of the way before continuing?"
      end


      output_template_in_target '.gitignore'
      output_template_in_target 'Rakefile'
      output_template_in_target 'LICENSE'
      output_template_in_target 'README.rdoc'
      output_template_in_target '.document'

      mkdir_in_target           lib_dir
      touch_in_target           File.join(lib_dir, "#{file_name_prefix}.rb")

      mkdir_in_target           test_dir
      output_template_in_target File.join(testing_framework.to_s, 'helper.rb'), File.join(test_dir, test_helper_filename)
      output_template_in_target File.join(testing_framework.to_s, 'flunking.rb'), File.join(test_dir, test_filename)

      if should_use_cucumber
        mkdir_in_target           features_dir
        output_template_in_target File.join(%w(features default.feature)), File.join('features', feature_filename)

        mkdir_in_target           features_support_dir
        output_template_in_target File.join(features_support_dir, 'env.rb')

        mkdir_in_target           features_steps_dir
        touch_in_target           File.join(features_steps_dir, steps_filename)
      end

    end

    def use_user_git_config
      git_config = self.read_git_config

      unless git_config.has_key? 'user.name'
        raise NoGitUserName
      end
      
      unless git_config.has_key? 'user.email'
        raise NoGitUserEmail
      end
      
      unless git_config.has_key? 'github.user'
        raise NoGitHubUser
      end
      
      unless git_config.has_key? 'github.token'
        raise NoGitHubToken
      end

      self.user_name       = git_config['user.name']
      self.user_email      = git_config['user.email']
      self.github_username = git_config['github.user']
      self.github_token    = git_config['github.token']
    end

    def output_template_in_target(source, destination = source)
      final_destination = File.join(target_dir, destination)

      template_contents = File.read(File.join(template_dir, source))
      template = ERB.new(template_contents, nil, '<>')

      template_result = template.result(binding)

      File.open(final_destination, 'w') {|file| file.write(template_result)}

      $stdout.puts "\tcreate\t#{destination}"
    end

    def template_dir
      File.join(File.dirname(__FILE__), 'templates')
    end

    def mkdir_in_target(directory)
      final_destination = File.join(target_dir, directory)

      FileUtils.mkdir final_destination

      $stdout.puts "\tcreate\t#{directory}"
    end

    def touch_in_target(destination)
      final_destination = File.join(target_dir, destination)
      FileUtils.touch  final_destination
      $stdout.puts "\tcreate\t#{destination}"
    end

    def gitify
      saved_pwd = Dir.pwd
      Dir.chdir(target_dir)
      begin
        begin
          @repo = Git.init()
        rescue Git::GitExecuteError => e
          raise GitInitFailed, "Encountered an error during gitification. Maybe the repo already exists, or has already been pushed to?"
        end

        begin
          @repo.add('.')
        rescue Git::GitExecuteError => e
          #raise GitAddFailed, "There was some problem adding this directory to the git changeset"
          raise
        end

        begin
          @repo.commit "Initial commit to #{github_repo_name}."
        rescue Git::GitExecuteError => e
          raise
        end

        begin
          @repo.add_remote('origin', github_remote)
        rescue Git::GitExecuteError => e
          puts "Encountered an error while adding origin remote. Maybe you have some weird settings in ~/.gitconfig?"
          raise
        end
      ensure
        Dir.chdir(saved_pwd)
      end
    end
    
    def create_and_push_repo
      Net::HTTP.post_form URI.parse('http://github.com/repositories'),
                                'login' => github_username,
                                'token' => github_token,
                                'repository[description]' => summary,
                                'repository[name]' => github_repo_name
      # TODO do a HEAD request to see when it's ready
      @repo.push('origin')
    end

    def enable_gem_for_repo
      url = "https://github.com/#{github_username}/#{github_repo_name}/update"
      `curl -F 'login=#{github_username}' -F 'token=#{github_token}' -F 'field=repository_rubygem' -F 'value=1' #{url} 2>/dev/null`
      # FIXME use NET::HTTP instead of curl
      #Net::HTTP.post_form URI.parse(url),
                                #'login' => github_username,
                                #'token' => github_token,
                                #'field' => 'repository_rubygem',
                                #'value' => '1'
    end

  end
end