Skip to content
Browse files

Initial release

  • Loading branch information...
0 parents commit 86130247157cdd6f26545dfde4138bfc074deafd @gravis gravis committed
18 .gitignore
@@ -0,0 +1,18 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
+config/gemnasium.yml
1 .rvmrc
@@ -0,0 +1 @@
+rvm ruby-1.9.3-p327@gemnasium-gem --create
3 CHANGELOG.md
@@ -0,0 +1,3 @@
+# 1.0.0 / 2013-03-04
+
+Initial release
4 Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in gemnasium-deployus.gemspec
+gemspec
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2013 Tech-Angels LLC
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
140 README.md
@@ -0,0 +1,140 @@
+# Gemnasium gem
+
+This gem lets you push your dependency files to [Gemnasium](https://gemnasium.com/) to track your project's dependencies and get notified about updates and security advisories.
+
+Gemnasium app offers Github integration with fully automated synchronization but you can use this gem if you don't want to authorize access to your repositories (ie: for privacy concern).
+
+Supported dependency files are:
+
+* Gemfile
+* Gemfile.lock
+* *.gemspec
+* package.json
+* npm-shrinkwrap.json
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+ gem 'gemnasium'
+
+Or in your terminal:
+
+ $ gem install gemnasium
+
+Add configuration file in your project
+
+ $ gemnasium install
+
+Install command supports 2 options : `--rake` and `--git` to respectively install the gemnasium [rake task](#2-via-the-rake-task) and a [post-commit git hook](#3-via-the-post-commit-git-hook).
+
+`gemnasium install` will add the config/gemnasium.yml file to your .gitignore so your private API key won't be committed. If you use another versionning system, please remember to ignore this file, especially for public project.
+
+__Warning: your api key is dedicated to your own user account and must not be published!__
+
+Fill the values of the new config/gemnasium.yml file.
+
+## Usage
+
+There is multiple ways to use the gemnasium gem. You can choose whichever you prefer.
+
+### 1. Via the command line
+
+Using gemnasium from the command line is as simple as typing `gemnasium [command]` :
+
+__To create a project on Gemnasium:__
+
+ $ gemnasium create
+
+Create command will look for data in your config/gemnasium.yml configuration file to create a project.
+If your project was previously managed automatically from Github or if you want to change the visibility of an existing project, you can use the `--force` option to overwrite existing setup.
+
+Please note that automatic Github synchronization will be dropped once project is configured with this gem.
+
+__To push your dependency files on Gemnasium:__
+
+ $ gemnasium push
+
+### 2. Via the rake task
+
+Gemnasium gem comes with a rake task ready to be used. To use it, you need to install it via: `gemnasium install --rake`
+Once installed, you'll have access to 2 tasks:
+
+__To create a project on Gemnasium:__
+
+ $ rake gemnasium:create
+
+Create command will look for data in your config/gemnasium.yml configuration file to create a project.
+If your project was previously managed automatically from Github or if you want to change the visibility of an existing project, you can use the `gemnasium:create:force` subtask to overwrite existing setup.
+
+Please note that automatic Github synchronization will be dropped once project is configured with this gem.
+
+__To push your dependency files on Gemnasium:__
+
+ $ rake gemnasium:push
+
+### 3. Via the post-commit git hook
+
+We wrote for you a ready-to-use [post-commit git hook](lib/templates/post-commit).
+
+Once installed via `gemnasium install --git`, the gem will push your dependency files after each commit only if they have changed.
+
+### 4. Directly in your code
+
+If you need to use Gemnasium gem right into your code, you can do so just like below:
+
+```ruby
+require 'gemnasium'
+
+
+# To install gemnasium files
+#
+# options is a Hash which can contain the following keys:
+# project_path (required) - [String] path to the project
+# install_rake_task - [Boolean] whether or not to install the rake task
+# install_git_hook - [Boolean] whether or not to install the git hook
+Gemnasium.install(options)
+
+# To create your project on gemnasium
+#
+# options is a Hash which can contain the following keys:
+# project_path (required) - [String] path to the project
+# overwrite_attr - [Boolean] whether or not to overwrite existing project's attributes
+Gemnasium.create_project(options)
+
+# To push supported dependency files to gemnasium
+#
+# options is a Hash which can contain the following keys:
+# project_path (required) - [String] path to the project
+Gemnasium.push(options)
+```
+
+## Sample config
+
+Here is a sample config file for our public project **tech-angels/vandamme** available at https://gemnasium.com/tech-angels/vandamme
+
+```yaml
+api_key: "some_secret_api_key"
+profile_name: "tech-angels"
+project_name: "vandamme"
+project_visibility: "public"
+project_branch: "master"
+```
+
+## Troubleshooting
+
+Gemnasium will try to display the most accurate error message when something goes wrong.
+
+Though, if you're stil stuck with something, feel free to contact [Gemnasium support](https://gemnasium.freshdesk.com).
+
+## Contributing
+
+1. Fork the project.
+2. Make your feature or bug fix.
+3. Test it.
+4. Commit.
+5. Create new pull request.
+
+## Credits
+
+[![Tech-Angels](http://media.tumblr.com/tumblr_m5ay3bQiER1qa44ov.png)](http://www.tech-angels.com)
6 Rakefile
@@ -0,0 +1,6 @@
+require "bundler/gem_tasks"
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new('spec')
+
+task :default => :spec
39 bin/gemnasium
@@ -0,0 +1,39 @@
+#!/usr/bin/env ruby
+#Adjust path in case called directly and not through gem
+$:.unshift "#{File.expand_path(File.dirname(__FILE__))}/../lib"
+
+require 'gemnasium'
+require 'gemnasium/options'
+require 'gemnasium/version'
+
+#Parse options
+begin
+ options, parser = Gemnasium::Options.parse ARGV
+rescue OptionParser::ParseError => e
+ $stderr.puts e.message.capitalize
+ $stderr.puts "Please see `gemnasium --help` for valid options"
+ abort
+end
+
+if options[:show_help]
+ puts parser
+ exit
+elsif options[:show_version]
+ puts "gemnasium v#{Gemnasium::VERSION}"
+ exit
+end
+
+# Set project path
+options[:project_path] = File.expand_path(".")
+
+case options[:command]
+when 'create'
+ Gemnasium.create_project options
+when 'install'
+ Gemnasium.install options
+when 'push'
+ Gemnasium.push options
+else
+ $stdout.puts "Please see `gemnasium --help` for valid options"
+ exit
+end
53 features/gemnasium.feature
@@ -0,0 +1,53 @@
+Feature: Help messages about gemnasium gem
+
+ By using gemnasium [options], the user is able to get helpfull messages
+ about how to use the gemnasium gem.
+
+ Scenario: Without options
+ When I run `gemnasium`
+ Then the output should contain "Please see `gemnasium --help` for valid options"
+ And the exit status should be 0
+
+ Scenario: With invalid option
+ When I run `gemnasium -z`
+ Then the output should contain exactly:
+ """
+ Invalid option: -z
+ Please see `gemnasium --help` for valid options\n
+ """
+ And the exit status should be 1
+
+ Scenario Outline: With version option
+ When I run `gemnasium <option>`
+ Then the output should contain exactly:
+ """
+ gemnasium v1.0.0\n
+ """
+ And the exit status should be 0
+
+ Scenarios: Version options
+ | option |
+ | -v |
+ | --version |
+
+ Scenario Outline: With version option
+ When I run `gemnasium <option>`
+ Then the output should contain exactly:
+ """
+ Usage: gemnasium [options]
+ -v, --version Show Gemnasium version
+ -h, --help Display this message
+
+ Available commands are:
+ create : Create or update project on Gemnasium
+ install : Install the necessary config file
+ push : Push your dependency files to Gemnasium
+
+ See `gemnasium COMMAND --help` for more information on a specific command.\n
+ """
+ And the exit status should be 0
+
+ Scenarios: Version options
+ | option |
+ | -h |
+ | --help |
12 features/gemnasium_create.feature
@@ -0,0 +1,12 @@
+Feature: Create or update a project on Gemnasium
+
+ By using gemnasium create [options], the user is able to create a new
+ project on Gemnasium or to update an existing one.
+
+ Scenario Outline: Without configuration file
+ Given a directory named "project/foo/bar"
+ And I cd to "project/foo/bar"
+ When I run `gemnasium create`
+ Then the output should contain "/config/gemnasium.yml) does not exist."
+ And the output should contain "Please run `gemnasium install`."
+ And the exit status should be 1
119 features/gemnasium_install.feature
@@ -0,0 +1,119 @@
+Feature: Create or update a project on Gemnasium
+
+ By using gemnasium install [options], the user is able to install
+ the necessary files to run gemnasium.
+
+ Scenario: Without option for a clean repo
+ Given a directory named "project/foo/bar"
+ And I cd to "project/foo/bar"
+ When I run `gemnasium install`
+ Then it should create the config directory
+ And it should create the config file
+ And the exit status should be 0
+
+ Scenario: Without option for a repo with a config directory
+ Given a directory named "project/foo/bar/config"
+ And I cd to "project/foo/bar"
+ When I run `gemnasium install`
+ Then it should create the config file
+ And the exit status should be 0
+
+ Scenario: Without option for a repo with the config file already installed
+ Given an empty file named "project/foo/bar/config/gemnasium.yml"
+ And I cd to "project/foo/bar"
+ When I run `gemnasium install`
+ Then the output should match:
+ """
+ The file .+\/config\/gemnasium.yml already exists
+ """
+ And the exit status should be 0
+
+ Scenario: With git option for a non git repo
+ Given a directory named "project/foo/bar"
+ And I cd to "project/foo/bar"
+ When I run `gemnasium install --git`
+ Then it should create the config directory
+ And it should create the config file
+ And the output should match:
+ """
+ .*\/project\/foo\/bar is not a git repository\. Try to run `git init`\.
+ """
+ And the file ".git/hooks/post-commit" should not exist
+ And the exit status should be 0
+
+ Scenario: With git option for git repo without post-commit hook
+ Given a directory named "project/foo/bar"
+ And I cd to "project/foo/bar"
+ And I run `git init`
+ When I run `gemnasium install --git`
+ Then it should create the config directory
+ And it should create the config file
+ And it should create the post-commit hook
+ And the exit status should be 0
+
+ Scenario: With git option for git repo with a post-commit hook file
+ Given a directory named "project/foo/bar"
+ And I cd to "project/foo/bar"
+ And I run `git init`
+ And an empty file named ".git/hooks/post-commit"
+ When I run `gemnasium install --git`
+ Then it should create the config directory
+ And it should create the config file
+ And the output should match:
+ """
+ The file .+\/.git\/hooks\/post-commit already exists
+ """
+ And the exit status should be 0
+
+ Scenario: With rake option for a repo without Rakefile
+ Given a directory named "project/foo/bar"
+ And I cd to "project/foo/bar"
+ When I run `gemnasium install --rake`
+ Then it should create the config directory
+ And it should create the config file
+ And the output should contain "Rakefile not found."
+ And the file "lib/tasks/gemnasium.rake" should not exist
+ And the exit status should be 0
+
+ Scenario: With rake option for a repo with a Rakefile without lib/tasks directory
+ Given an empty file named "project/foo/bar/Rakefile"
+ And I cd to "project/foo/bar"
+ When I run `gemnasium install --rake`
+ Then it should create the config directory
+ And it should create the config file
+ And it should create the tasks directory
+ And it should create the task file
+ And the exit status should be 0
+
+ Scenario: With rake option for a repo with a Rakefile with a lib/tasks directory
+ Given an empty file named "project/foo/bar/Rakefile"
+ And a directory named "project/foo/bar/lib/tasks"
+ And I cd to "project/foo/bar"
+ When I run `gemnasium install --rake`
+ Then it should create the config directory
+ And it should create the config file
+ And it should create the task file
+ And the exit status should be 0
+
+ Scenario: With rake option for a repo with the rake tasks already installed
+ Given an empty file named "project/foo/bar/Rakefile"
+ And an empty file named "project/foo/bar/lib/tasks/gemnasium.rake"
+ And I cd to "project/foo/bar"
+ When I run `gemnasium install --rake`
+ Then it should create the config directory
+ And it should create the config file
+ And the output should match:
+ """
+ The file .+\/lib\/tasks\/gemnasium\.rake already exists
+ """
+ And the exit status should be 0
+
+ Scenario: With both rake and git options
+ Given an empty file named "project/foo/bar/Rakefile"
+ And I cd to "project/foo/bar"
+ And I run `git init`
+ When I run `gemnasium install --git --rake`
+ Then it should create the config directory
+ And it should create the config file
+ And it should create the task file
+ And it should create the post-commit hook
53 features/step_definitions/gemnasium_steps.rb
@@ -0,0 +1,53 @@
+Then /^it should create the config directory$/ do
+ steps %{
+ Then the output should match:
+ """
+ Creating config directory
+ """
+ And a directory named "config" should exist
+ }
+end
+Then /^it should create the config file$/ do
+ steps %{
+ Then the output should match:
+ """
+ File created in .*\/config\/gemnasium\.yml\.
+ Please fill configuration file with accurate values\.
+ """
+ And a file named "config/gemnasium.yml" should exist
+ }
+end
+Then /^it should create the post-commit hook$/ do
+ steps %{
+ Then the output should match:
+ """
+ File created in .*\/.git\/hooks\/post-commit\.
+ """
+ And a file named ".git/hooks/post-commit" should exist
+ }
+end
+Then /^it should create the task file$/ do
+ steps %{
+ Then the output should match:
+ """
+ File created in .*\/lib\/tasks\/gemnasium.rake.
+ """
+ And the output should contain:
+ """
+ Usage:
+ rake gemnasium:push - to push your dependency files
+ rake gemnasium:create - to create your project on Gemnasium
+ rake gemnasium:create:force - to overwrite already existing Gemnasium project attributes
+ """
+ And a file named "lib/tasks/gemnasium.rake" should exist
+ }
+end
+Then /^it should create the tasks directory$/ do
+ steps %{
+ Then the output should match:
+ """
+ Creating lib/tasks directory.
+ """
+ And a directory named "lib/tasks" should exist
+ }
+end
7 features/support/env.rb
@@ -0,0 +1,7 @@
+require 'aruba/cucumber'
+require 'fileutils'
+
+After do
+ test_project_path = "#{File.dirname(__FILE__)}../../../tmp/aruba/project"
+ FileUtils.rm_r File.expand_path(test_project_path) if File.exists? test_project_path
+end
22 gemnasium.gemspec
@@ -0,0 +1,22 @@
+require './lib/gemnasium/version'
+
+Gem::Specification.new do |gem|
+ gem.authors = ["Tech-Angels"]
+ gem.email = ["contact@tech-angels.com"]
+ gem.description = "Safely upload your ruby dependency files (Gemfile, Gemfile.lock, *.gemspec, package.json, npm-shrinkwrap.json) on gemnasium.com to track dependencies and get notified about updates and security advisories."
+ gem.summary = gem.description
+ gem.homepage = "https://gemnasium.com/"
+
+ gem.files = `git ls-files`.split($\)
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
+ gem.name = "gemnasium"
+ gem.require_paths = ["lib"]
+ gem.executables = ["gemnasium"]
+ gem.version = Gemnasium::VERSION
+
+ gem.add_development_dependency 'rake', '~>10.0.3'
+ gem.add_development_dependency 'rspec'
+ gem.add_development_dependency 'cucumber'
+ gem.add_development_dependency 'aruba'
+ gem.add_development_dependency 'webmock'
+end
226 lib/gemnasium.rb
@@ -0,0 +1,226 @@
+require 'json'
+require 'gemnasium/connection'
+require 'gemnasium/configuration'
+require 'gemnasium/dependency_files'
+require 'gemnasium/errors'
+
+module Gemnasium
+ class << self
+
+ # Push dependency files to Gemnasium
+ # @param options [Hash] Parsed options from the command line. Options supported:
+ # * :project_path - Path to the project (required)
+ def push options
+ @config = load_config(options[:project_path])
+
+ unless current_branch == @config.project_branch
+ quit_because_of("Gemnasium : Dependency files updated but not on tracked branch (#{@config.project_branch}), ignoring...\n")
+ end
+
+ dependency_files_hashes = DependencyFiles.get_sha1s_hash(options[:project_path])
+ quit_because_of("No supported dependency files detected.") if dependency_files_hashes.empty?
+ notify "#{dependency_files_hashes.keys.count} supported dependency file(s) found: #{dependency_files_hashes.keys.join(', ')}"
+
+ # Ask to Gemnasium server which dependency file should be uploaded (new or modified)
+ comparison_results = request("#{connection.api_path_for('dependency_files')}/compare", dependency_files_hashes)
+ files_to_upload = comparison_results['to_upload']
+ deleted_files = comparison_results['deleted']
+ notify "#{deleted_files.count} deleted file(s): #{deleted_files.join(', ')}", :blue unless deleted_files.empty?
+
+ unless files_to_upload.empty?
+ notify "#{files_to_upload.count} file(s) to upload: #{files_to_upload.join(', ')}"
+
+ # Upload requested dependency files content
+ upload_summary = request("#{connection.api_path_for('dependency_files')}/upload", DependencyFiles.get_content_to_upload(options[:project_path], files_to_upload))
+ notify "Added dependency files: #{upload_summary['added']}", :green
+ notify "Updated dependency files: #{upload_summary['updated']}", :green
+ notify "Unchanged dependency files: #{upload_summary['unchanged']}", :blue unless upload_summary['unchanged'].empty?
+ notify "Unsupported dependency files: #{upload_summary['unsupported']}", :blue unless upload_summary['unsupported'].empty?
+ else
+ notify "The project's dependency files are up-to-date.", :blue
+ end
+ rescue => exception
+ quit_because_of(exception.message)
+ end
+
+ # Install needed file(s) to run gemnasium command
+ #
+ # @param options [Hash] Parsed options from the command line. Options supported:
+ # * :install_rake_task - Install a rake task to the project
+ # * :install_git_hook - Install a git post-commit hook
+ # * :project_path - Path to the project (required)
+ def install options
+ require 'fileutils'
+
+ # Install config file
+ config_file_dir = "#{options[:project_path]}/config"
+
+ unless File.exists? config_file_dir
+ notify "Creating config directory"
+ FileUtils.mkdir_p config_file_dir
+ end
+
+ # Try to add config/gemnasium.yml to .gitignore
+ if File.exists? "#{options[:project_path]}/.gitignore"
+ File.open("#{options[:project_path]}/.gitignore", 'a+') do |f|
+ f.write("\n# Gemnasium gem configuration file\nconfig/gemnasium.yml") unless f.read.include? 'config/gemnasium.yml'
+ notify "Configuration file added to your project's .gitignore."
+ end
+ end
+
+ notify 'Please fill configuration file with accurate values.', :blue if copy_template('gemnasium.yml', "#{config_file_dir}/gemnasium.yml")
+
+ # Install git hook
+ if options[:install_git_hook]
+ notify ''
+ if File.exists? "#{options[:project_path]}/.git/hooks"
+ copy_template('post-commit', "#{options[:project_path]}/.git/hooks/post-commit")
+ else
+ notify "#{options[:project_path]} is not a git repository. Try to run `git init`.", :red
+ end
+ end
+
+ # Install rake task
+ if options[:install_rake_task]
+ notify ''
+ if !File.exists? "#{options[:project_path]}/Rakefile"
+ notify "Rakefile not found.", :red
+ else
+ rake_file_dir = "#{options[:project_path]}/lib/tasks"
+
+ unless File.exists? rake_file_dir
+ notify "Creating lib/tasks directory"
+ FileUtils.mkdir_p rake_file_dir
+ end
+
+ if copy_template('gemnasium.rake', "#{rake_file_dir}/gemnasium.rake")
+ notify 'Usage:', :blue
+ notify "\trake gemnasium:push \t\t- to push your dependency files", :blue
+ notify "\trake gemnasium:create \t\t- to create your project on Gemnasium", :blue
+ notify "\trake gemnasium:create:force - to overwrite already existing Gemnasium project attributes", :blue
+ end
+ end
+ end
+ end
+
+ # Create the project on Gemnasium
+ #
+ # @param options [Hash] Parsed options from the command line. Options supported:
+ # * :overwrite_attr - Force Gemnasium to overwrite the project attributes.
+ # * :project_path - Path to the project (required)
+ def create_project options
+ @config = load_config(options[:project_path])
+
+ project_params = { name: @config.project_name, privacy: @config.project_visibility, branch: @config.project_branch}
+ project_params.merge!({ overwrite_attributes: true }) if !!options[:overwrite_attr]
+
+ creation_result = request("#{connection.api_path_for('projects')}", project_params)
+
+ notify "#{creation_result['is_private'] ? 'Private' : 'Public'} project `#{creation_result['name']}` successfully created for #{creation_result['profile']}.", :green
+ notify "Remaining private slots for this profile: #{creation_result['remaining_slot']}", :blue
+ rescue => exception
+ quit_because_of(exception.message)
+ end
+
+ def config
+ @config || quit_because_of('No configuration file loaded')
+ end
+
+ private
+
+ # Issue a HTTP request
+ #
+ # @params path [String] Path of the request
+ # parameters [Hash] Parameters to send a POST request
+ def request(path, parameters = {})
+ if parameters.empty?
+ response = connection.get(path)
+ else
+ response = connection.post(path, JSON.generate(parameters))
+ end
+
+ raise Gemnasium::InvalidApiKeyError if response.code.to_i == 401
+
+ response_body = JSON.parse(response.body)
+
+ if response.code.to_i / 100 == 2
+ return {} if response_body.empty?
+ result = response_body
+ else
+ if error = "#{response_body['error']}_error".split('_').collect(&:capitalize).join
+ raise Gemnasium.const_get(error), response_body['message']
+ else
+ quit_because_of 'An unknown error has been returned by the server. Please contact Gemnasium support : http://support.gemnasium.com'
+ end
+ end
+ end
+
+ # Create a connection
+ def connection
+ @connection ||= Connection.new
+ end
+
+ # Load config from config file
+ #
+ # @param config_file [String] path to the project
+ # @return [Hash] config options
+ def load_config project_path
+ @config ||= Configuration.new("#{project_path}/config/gemnasium.yml")
+ rescue => exception
+ quit_because_of(exception.message)
+ end
+
+ # Puts a message to the standard output
+ #
+ # @param message [String] message to display
+ def notify message, color = nil
+ if $stdout.tty? && !color.nil?
+ color_code = { red: "\e[31m", green: "\e[32m", blue: "\e[34m" }
+ reset_color = "\e[0m"
+
+ message = color_code[color] + message + reset_color
+ end
+
+ $stdout.puts message
+ end
+
+ # Abort the program and colorize the message if $stderr is tty
+ #
+ # @param error_message [String] message to be puts to $stderr
+ def quit_because_of(error_message)
+ error_message = "\e[31m#{error_message}\e[0m" if $stderr.tty?
+ abort error_message
+ end
+
+ # Get the current git branch
+ #
+ # @return [String] name of the current branch
+ def current_branch
+ branch = `git branch 2>/dev/null`.split("\n").delete_if { |branch| branch.chars.first != "*" }.first
+ branch.gsub("* ","") unless branch.nil?
+ end
+
+ # Copy a template file
+ #
+ # @param file [String] template to copy
+ # target_path [String] location where to copy the template
+ def copy_template(file, target_path)
+ if File.exists? target_path
+ notify "The file #{target_path} already exists"
+ else
+ template_file = File.expand_path("#{File.dirname(__FILE__)}/templates/#{file}")
+ FileUtils.cp template_file, target_path
+
+ if File.exists? target_path
+ notify "File created in #{target_path}.", :green
+
+ return true
+ else
+ notify "Could not install #{file} file.", :red
+ end
+ end
+
+ false
+ end
+ end
+end
45 lib/gemnasium/configuration.rb
@@ -0,0 +1,45 @@
+require 'yaml'
+
+module Gemnasium
+ class Configuration
+ attr_accessor :site, :api_key, :use_ssl, :profile_name, :project_name, :project_visibility, :api_version, :project_branch
+ DEFAULT_CONFIG = { site: 'gemnasium.com',
+ use_ssl: true,
+ api_version: 'v1',
+ project_visibility: 'public' }
+
+ # Initialize the configuration object from a YAML file
+ #
+ # @param config_file [String] path to the configuration file
+ def initialize config_file
+ raise Errno::ENOENT, "Configuration file (#{config_file}) does not exist.\nPlease run `gemnasium install`." unless File.file?(config_file)
+
+ config_hash = DEFAULT_CONFIG.merge!(YAML.load_file(config_file))
+ config_hash.each do |k, v|
+ writer_method = "#{k}="
+ send(writer_method, v) if respond_to?(writer_method)
+ end
+
+ raise 'Your configuration file does not contain all mandatory parameters or contain invalid values. Please check the documentation.' unless is_valid?
+ end
+
+ private
+
+ # Check that mandatory parameters are not nil and contain valid values
+ #
+ # @return [Boolean] if configuration is valid
+ def is_valid?
+ site_option_valid = !site.nil? && !site.empty?
+ api_key_option_valid = !api_key.nil? && !api_key.empty?
+ use_ssl_option_valid = !use_ssl.nil? && !!use_ssl == use_ssl # Check this is a boolean
+ api_version_option_valid = !api_version.nil? && !api_version.empty?
+ profile_name_option_valid = !profile_name.nil? && !profile_name.empty?
+ project_name_option_valid = !project_name.nil? && !project_name.empty?
+ project_visibility_option_valid = !project_visibility.nil? && !project_visibility.empty? &&
+ (project_visibility == 'public' || project_visibility == 'private')
+
+ site_option_valid && api_key_option_valid && use_ssl_option_valid && api_version_option_valid &&
+ profile_name_option_valid && project_name_option_valid && project_visibility_option_valid
+ end
+ end
+end
42 lib/gemnasium/connection.rb
@@ -0,0 +1,42 @@
+require 'net/https'
+
+module Gemnasium
+ class Connection
+ def initialize
+ @connection = Net::HTTP.new(Gemnasium.config.site, Gemnasium.config.use_ssl ? 443 : 80)
+ @connection.use_ssl = Gemnasium.config.use_ssl
+ end
+
+ def post(path, body, headers = {})
+ request = Net::HTTP::Post.new(path, headers.merge('Accept' => 'application/json', 'Content-Type' => 'application/json'))
+ request.basic_auth('X', Gemnasium.config.api_key)
+ request.body = body
+ @connection.request(request)
+ end
+
+ def get(path, headers = {})
+ request = Net::HTTP::Get.new(path, headers.merge('Accept' => 'application/json', 'Content-Type' => 'application/json'))
+ request.basic_auth('X', Gemnasium.config.api_key)
+ @connection.request(request)
+ end
+
+ # Set the API path for a specific item
+ #
+ # @param item [String] item the route should point to
+ # @return [String] API path
+ def api_path_for item
+ base = "/api/#{Gemnasium.config.api_version}"
+
+ case item
+ when 'base'
+ base
+ when 'projects'
+ "#{base}/profiles/#{Gemnasium.config.profile_name}/projects"
+ when 'dependency_files'
+ "#{base}/profiles/#{Gemnasium.config.profile_name}/projects/#{Gemnasium.config.project_name}/dependency_files"
+ else
+ raise "No API path found for #{item}"
+ end
+ end
+ end
+end
44 lib/gemnasium/dependency_files.rb
@@ -0,0 +1,44 @@
+require 'digest/sha1'
+
+module Gemnasium
+ class DependencyFiles
+
+ SUPPORTED_DEPENDENCY_FILES = /^(Gemfile|Gemfile\.lock|.*\.gemspec|package\.json|npm-shrinkwrap\.json)$/
+
+ # Get a Hash of sha1s for each file corresponding to the regex
+ #
+ # @param regexp [Regexp] the regular expression of requested files
+ # @return [Hash] the hash associating each file path with its SHA1 hash
+ def self.get_sha1s_hash(project_path)
+ Dir.chdir(project_path)
+ Dir.glob("**/**").grep(SUPPORTED_DEPENDENCY_FILES).inject({}) do |h, file_path|
+ h[file_path] = calculate_sha1("#{project_path}/#{file_path}")
+ h
+ end
+ end
+
+ # Get the content to upload to Gemnasium.
+ #
+ # @param files_path [Array] an array containing the path of the files
+ # @return [Array] array of hashes containing file name, file sha and file content
+ def self.get_content_to_upload(project_path, files_path)
+ files_path.inject([]) do |arr, file_path|
+ arr << { filename: file_path, sha: calculate_sha1(file_path), content: File.open("#{project_path}/#{file_path}") {|io| io.read} }
+ end
+ end
+
+ private
+
+ # Calculate hash of a file in the same way git does
+ #
+ # @param file_path [String] path of the file
+ # @return [String] SHA1 of the file
+ def self.calculate_sha1(file_path)
+ mem_buf = File.open(file_path) {|io| io.read}
+ size = mem_buf.size
+ header = "blob #{size}\0" # type[space]size[null byte]
+
+ Digest::SHA1.hexdigest(header + mem_buf)
+ end
+ end
+end
19 lib/gemnasium/errors.rb
@@ -0,0 +1,19 @@
+module Gemnasium
+ class InvalidApiKeyError < StandardError
+ def message
+ 'Your API key is invalid. Please double check it on https://gemnasium.com/settings/api_access'
+ end
+ end
+ class DeprecatedApiVersionError < StandardError; end
+ # Profile errors
+ class ProfileNotFoundError < StandardError; end
+ class ProfileNotOwnedError < StandardError; end
+ class NoSlotsAvailableError < StandardError; end
+ # Project errors
+ class ProjectNotFoundError < StandardError; end
+ class ProjectNotCreatedError < StandardError; end
+ class ProjectParamMissingError < StandardError; end
+ class ProjectAlreadyExistsError < StandardError; end
+ class ProjectGithubSyncedError < StandardError; end
+ class ProjectBranchMismatchError < StandardError; end
+end
84 lib/gemnasium/options.rb
@@ -0,0 +1,84 @@
+require 'optparse'
+
+module Gemnasium
+ class Options
+
+ # Parse arguments from command line
+ #
+ # @param args [Array] arguments from command line
+ # @return [Hash, OptionParser] hash of the parsed options & Option parser to get --help message
+ def self.parse args
+ options = {}
+
+ global = OptionParser.new do |opts|
+ opts.banner = 'Usage: gemnasium [options]'
+
+ opts.on '-v', '--version', 'Show Gemnasium version' do
+ options[:show_version] = true
+ end
+
+ opts.on '-h', '--help', 'Display this message' do
+ options[:show_help] = true
+ end
+
+ opts.separator ''
+ opts.separator <<-HELP_MESSAGE
+Available commands are:
+ create : Create or update project on Gemnasium
+ install : Install the necessary config file
+ push : Push your dependency files to Gemnasium
+
+See `gemnasium COMMAND --help` for more information on a specific command.
+ HELP_MESSAGE
+ end
+
+ subcommands = {
+ 'create' => OptionParser.new do |opts|
+ opts.banner = 'Usage: gemnasium create [options]'
+
+ opts.on('--force', "Force overwriting project's attributes if it already exists") do
+ options[:overwrite_attr] = true
+ end
+
+ opts.on '-h', '--help', 'Display this message' do
+ options[:show_help] = true
+ end
+ end,
+ 'install' => OptionParser.new do |opts|
+ opts.banner = 'Usage: gemnasium install [options]'
+
+ opts.on '--git', 'Create a post-commit hook to run gemnasium push command if a dependency file has been commited' do
+ options[:install_git_hook] = true
+ end
+
+ opts.on '--rake', 'Create rake task to run Gemnasium' do
+ options[:install_rake_task] = true
+ end
+
+ opts.on '-h', '--help', 'Display this message' do
+ options[:show_help] = true
+ end
+ end,
+ 'push' => OptionParser.new do |opts|
+ opts.banner = 'Usage: gemnasium push'
+
+ opts.on '-h', '--help', 'Display this message' do
+ options[:show_help] = true
+ end
+ end
+ }
+
+ global.order! args
+ parser = global
+
+ unless (command = args.shift).nil?
+ raise OptionParser::ParseError unless subcommands.has_key?(command)
+ subcommands[command].order! args
+ options[:command] = command
+ parser = subcommands[command]
+ end
+
+ return options, parser
+ end
+ end
+end
3 lib/gemnasium/version.rb
@@ -0,0 +1,3 @@
+module Gemnasium
+ VERSION = "1.0.0"
+end
20 lib/templates/gemnasium.rake
@@ -0,0 +1,20 @@
+namespace :gemnasium do
+ require 'gemnasium'
+
+ desc "Push dependency files to gemnasium"
+ task :push do
+ Gemnasium.push project_path: File.expand_path(".")
+ end
+
+ desc "Create project on gemnasium"
+ task :create do
+ Gemnasium.create_project project_path: File.expand_path(".")
+ end
+
+ namespace :create do
+ desc "Force project creation/update on gemnasium"
+ task :force do
+ Gemnasium.create_project project_path: File.expand_path("."), overwrite_attr: true
+ end
+ end
+end
5 lib/templates/gemnasium.yml
@@ -0,0 +1,5 @@
+api_key: "api_key_goes_here" # You personal (secret) API key. Get it at https://gemnasium.com/settings/api_access
+profile_name: "profile_name" # One of https://gemnasium.com/settings/github_setup
+project_name: "project_name" # Will be available at https://gemnasium.com/profile_name/project_name
+project_visibility: "private" # Either public or private
+project_branch: "master" # /!\ If you don't use git, remove this line
7 lib/templates/post-commit
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+SUPPORTED_COMMITED_FILES=$((git diff --name-only HEAD~ HEAD 2>/dev/null || git diff-tree -r --no-commit-id --name-only --root HEAD) | grep -P '^(Gemfile|Gemfile.lock|.*\.gemspec|package.json|npm-shrinkwrap.json)$')
+
+if [[ $SUPPORTED_COMMITED_FILES > 0 ]]; then
+ gemnasium push
+fi
50 spec/gemnasium/configuration_spec.rb
@@ -0,0 +1,50 @@
+require 'fileutils'
+require 'spec_helper'
+
+describe Gemnasium::Configuration do
+ describe 'default config' do
+ it { expect(Gemnasium::Configuration::DEFAULT_CONFIG[:site]).to eql 'gemnasium.com' }
+ it { expect(Gemnasium::Configuration::DEFAULT_CONFIG[:use_ssl]).to eql true }
+ it { expect(Gemnasium::Configuration::DEFAULT_CONFIG[:api_version]).to eql 'v1' }
+ it { expect(Gemnasium::Configuration::DEFAULT_CONFIG[:project_visibility]).to eql 'public' }
+ end
+
+ describe 'initialize' do
+ let(:config_file_path) { 'tmp/config.yml' }
+
+ context 'for an inexistant config file' do
+ it { expect { Gemnasium::Configuration.new File.expand_path(config_file_path) }.to raise_error Errno::ENOENT }
+ end
+
+ context 'for a config file that does exist' do
+ before { FileUtils.touch(config_file_path) }
+ after { File.delete(config_file_path) }
+
+ context 'with missing mandatory values' do
+ let(:config_options) {{ profile_name: 'tech-angels', project_name: 'gemnasium-gem' }}
+ before do
+ File.open(config_file_path, 'w+') { |f| f.write(config_options.to_yaml) }
+ end
+
+ it { expect { Gemnasium::Configuration.new File.expand_path(config_file_path) }.to raise_error('Your configuration file does not contain all mandatory parameters or contain invalid values. Please check the documentation.') }
+ end
+
+ context 'with all mandatory values' do
+ let(:config_options) {{ api_key: 'api_key', profile_name: 'tech-angels', project_name: 'gemnasium-gem', project_branch: 'master' }}
+ before do
+ File.open(config_file_path, 'w+') { |f| f.write(config_options.to_yaml) }
+ end
+ let(:config) { Gemnasium::Configuration.new File.expand_path(config_file_path) }
+
+ it { expect(config.api_key).to eql config_options[:api_key] }
+ it { expect(config.profile_name).to eql config_options[:profile_name] }
+ it { expect(config.project_name).to eql config_options[:project_name] }
+ it { expect(config.project_branch).to eql config_options[:project_branch] }
+ it { expect(config.site).to eql Gemnasium::Configuration::DEFAULT_CONFIG[:site] }
+ it { expect(config.use_ssl).to eql Gemnasium::Configuration::DEFAULT_CONFIG[:use_ssl] }
+ it { expect(config.api_version).to eql Gemnasium::Configuration::DEFAULT_CONFIG[:api_version] }
+ it { expect(config.project_visibility).to eql Gemnasium::Configuration::DEFAULT_CONFIG[:project_visibility] }
+ end
+ end
+ end
+end
44 spec/gemnasium/connection_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Gemnasium::Connection do
+ before { stub_config && stub_requests }
+ let(:connection) { Gemnasium::Connection.new }
+
+ describe 'initialize' do
+ it 'initializes a Net::HTTP object' do
+ connection.instance_variable_get('@connection').should be_kind_of(Net::HTTP)
+ end
+ end
+
+ describe 'get' do
+ before { connection.get('/test_path') }
+
+ it 'issues a GET request' do
+ expect(WebMock).to have_requested(:get, api_url('/test_path'))
+ .with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ end
+ end
+
+ describe 'post' do
+ before { connection.post('/test_path', { foo: 'bar' }.to_json) }
+
+ it 'issues a POST request' do
+ expect(WebMock).to have_requested(:post, api_url('/test_path'))
+ .with(:body => {"foo"=>"bar"}, :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ end
+ end
+
+ describe 'api_path_for' do
+ context 'base API path' do
+ it{ expect(connection.api_path_for('base')).to eql "/api/#{Gemnasium.config.api_version}" }
+ end
+
+ context 'projects API path' do
+ it{ expect(connection.api_path_for('projects')).to eql "/api/#{Gemnasium.config.api_version}/profiles/#{Gemnasium.config.profile_name}/projects" }
+ end
+
+ context 'dependency files API path' do
+ it{ expect(connection.api_path_for('dependency_files')).to eql "/api/#{Gemnasium.config.api_version}/profiles/#{Gemnasium.config.profile_name}/projects/#{Gemnasium.config.project_name}/dependency_files" }
+ end
+ end
+end
47 spec/gemnasium/dependency_files_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Gemnasium::DependencyFiles do
+ let(:project_path) { File.expand_path("#{File.dirname(__FILE__)}../../../")}
+
+ describe 'get_sha1s_hash' do
+ context 'with a non matching regexp' do
+ it 'returns an empty Hash' do
+ sha1s_hash = Gemnasium::DependencyFiles.get_sha1s_hash("#{project_path}/tmp")
+
+ expect(sha1s_hash).to be_kind_of Hash
+ expect(sha1s_hash).to be_empty
+ end
+ end
+
+ context 'with a mathing regexp' do
+ it 'returns a Hash of matching files and their git calculated hash' do
+ sha1s_hash = Gemnasium::DependencyFiles.get_sha1s_hash(project_path)
+
+ expect(sha1s_hash).to include({ 'gemnasium.gemspec' => git_hash('gemnasium.gemspec') })
+ end
+ end
+ end
+
+ describe 'get_content_to_upload' do
+ context 'with no files' do
+ it 'returns an empty Hash' do
+ content_to_upload = Gemnasium::DependencyFiles.get_content_to_upload(project_path, [])
+
+ expect(content_to_upload).to be_kind_of Array
+ expect(content_to_upload).to be_empty
+ end
+ end
+
+ context 'with a mathing regexp' do
+ it 'returns a Hash of matching files and their git calculated hash' do
+ content_to_upload = Gemnasium::DependencyFiles.get_content_to_upload(project_path, ['gemnasium.gemspec'])
+
+ expect(content_to_upload).to eql([{ filename: 'gemnasium.gemspec', sha: git_hash('gemnasium.gemspec'), content: File.open('gemnasium.gemspec') {|io| io.read} }])
+ end
+ end
+ end
+end
+
+def git_hash(path)
+ %x( git hash-object #{path} ).strip
+end
110 spec/gemnasium/options_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Gemnasium::Options do
+ describe 'parse' do
+ context 'without options' do
+ it 'does not parse any option' do
+ options, parser = Gemnasium::Options.parse []
+ expect(options).to be_empty
+ expect(parser).to be_kind_of OptionParser
+ end
+ end
+
+ context 'with version options' do
+ it 'understands short version' do
+ options, parser = Gemnasium::Options.parse ['-v']
+ expect(options).to eql({ show_version: true })
+ end
+
+ it 'understands long version' do
+ options, parser = Gemnasium::Options.parse ['--version']
+ expect(options).to eql({ show_version: true })
+ end
+ end
+
+ context 'with help options' do
+ it 'understands short version' do
+ options, parser = Gemnasium::Options.parse ['-h']
+ expect(options).to eql({ show_help: true })
+ end
+
+ it 'understands long version' do
+ options, parser = Gemnasium::Options.parse ['--help']
+ expect(options).to eql({ show_help: true })
+ end
+ end
+
+ context 'with multiple options' do
+ it 'understands concatenated options' do
+ options, parser = Gemnasium::Options.parse ['-hv']
+ expect(options).to eql({ show_help: true, show_version: true })
+ end
+
+ it 'understands separated options' do
+ options, parser = Gemnasium::Options.parse ['-v', '--help']
+ expect(options).to eql({ show_help: true, show_version: true })
+ end
+ end
+
+ context 'with unsupported option' do
+ it 'raises an error' do
+ expect { Gemnasium::Options.parse ['--foo'] }.to raise_error OptionParser::ParseError
+ end
+ end
+
+ context 'with unsupported subcommand' do
+ it 'raises an error' do
+ expect { Gemnasium::Options.parse ['hack'] }.to raise_error OptionParser::ParseError
+ end
+ end
+
+ context 'with unsupported option for a valid subcommand' do
+ it 'raises an error' do
+ expect { Gemnasium::Options.parse ['install', '--foo'] }.to raise_error OptionParser::ParseError
+ end
+ end
+
+ context 'with valid subcommand' do
+ context '`create`' do
+ context 'with no options' do
+ it 'correctly set the options' do
+ options, parser = Gemnasium::Options.parse ['create']
+ expect(options).to eql({ command: 'create' })
+ end
+ end
+
+ context 'with force option' do
+ it 'correctly set the options' do
+ options, parser = Gemnasium::Options.parse ['create', '--force']
+ expect(options).to eql({ command: 'create', overwrite_attr: true })
+ end
+ end
+ end
+
+ context '`install`' do
+ context 'with no options' do
+ it 'correctly set the options' do
+ options, parser = Gemnasium::Options.parse ['install']
+ expect(options).to eql({ command: 'install' })
+ end
+ end
+
+ context 'with rake option' do
+ it 'correctly set the options' do
+ options, parser = Gemnasium::Options.parse ['install', '--rake']
+ expect(options).to eql({ command: 'install', install_rake_task: true })
+ end
+ end
+ end
+
+ context '`push`' do
+ context 'with no options' do
+ it 'correctly set the options' do
+ options, parser = Gemnasium::Options.parse ['push']
+ expect(options).to eql({ command: 'push' })
+ end
+ end
+ end
+ end
+ end
+end
349 spec/gemnasium_spec.rb
@@ -0,0 +1,349 @@
+require 'spec_helper'
+
+shared_examples_for 'an installed file' do
+ it 'creates the configuration file' do
+ expect(File.exists? target_path).to be_true
+ # Test that the files are identical comparing their MD5 hashes
+ template_file_md5 = Digest::MD5.hexdigest(File.read(template_path))
+ new_file_md5 = Digest::MD5.hexdigest(File.read(target_path))
+ expect(new_file_md5).to eql template_file_md5
+ end
+
+ it 'informs the user that the file has been created' do
+ expect(output).to include "File created in #{target_path}."
+ end
+end
+
+describe Gemnasium do
+ let(:output) { [] }
+ let(:error_output) { [] }
+ let(:project_path) { File.expand_path("#{File.dirname(__FILE__)}../../tmp") }
+ before do
+ Gemnasium.stub(:notify) { |arg| output << arg }
+ Gemnasium.stub(:quit_because_of) { |arg| error_output << arg && abort }
+ stub_config && stub_requests
+ end
+
+ describe 'push' do
+ context 'on a non tracked branch' do
+ before { Gemnasium.stub(:current_branch).and_return('non_project_branch') }
+
+ it 'quit the program' do
+ expect{ Gemnasium.push({ project_path: project_path }) }.to raise_error { |e|
+ expect(e).to be_kind_of SystemExit
+ expect(error_output).to include "Gemnasium : Dependency files updated but not on tracked branch (master), ignoring...\n"
+ }
+ end
+ end
+
+ context 'on the tracked branch' do
+ before { Gemnasium.stub(:current_branch).and_return('master') }
+
+ context 'with no supported dependency files found' do
+ before { Gemnasium::DependencyFiles.stub(:get_sha1s_hash).and_return([]) }
+
+ it 'quit the program with an error' do
+ expect{ Gemnasium.push({ project_path: project_path }) }.to raise_error { |e|
+ expect(e).to be_kind_of SystemExit
+ expect(error_output).to include "No supported dependency files detected."
+ }
+ end
+ end
+
+ context 'with supported dependency files found' do
+ let(:sha1_hash) {{ 'new_gemspec.gemspec' => 'gemspec_sha1_hash', 'modified_lockfile.lock' => 'lockfile_sha1_hash', 'Gemfile_unchanged.lock' => 'gemfile_sha1_hash' }}
+ before { Gemnasium::DependencyFiles.stub(:get_sha1s_hash).and_return(sha1_hash) }
+
+ context 'for a gemnasium project already up-to-date' do
+ before do
+ stub_config({ project_name: 'up_to_date_project' })
+ Gemnasium.push({ project_path: project_path })
+ end
+
+ it 'quit the program with an error' do
+ expect(output).to include "The project's dependency files are up-to-date."
+ end
+ end
+
+ context 'for gemnasium project not up-to-date' do
+ let(:hash_to_upload) {[{ filename: 'new_gemspec.gemspec', sha: 'gemspec_sha1_hash', content: 'stubbed gemspec content' },
+ { filename: 'modified_lockfile.lock', sha: 'lockfile_sha1_hash', content: 'stubbed lockfile content' }]}
+ before do
+ Gemnasium::DependencyFiles.stub(:get_content_to_upload).and_return(hash_to_upload)
+ Gemnasium.push({ project_path: project_path })
+ end
+
+ it 'informs the user that it found supported dependency files' do
+ expect(output).to include "#{sha1_hash.keys.count} supported dependency file(s) found: #{sha1_hash.keys.join(', ')}"
+ end
+
+ it 'makes a request to Gemnasium to get updated files to upload' do
+ expect(WebMock).to have_requested(:post, api_url('/api/v1/profiles/tech-angels/projects/gemnasium-gem/dependency_files/compare'))
+ .with(:body => sha1_hash.to_json,
+ :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ end
+
+ it 'informs the user that Gemnasium has deleted an old dependency file' do
+ expect(output).to include "1 deleted file(s): old_dependency_file"
+ end
+
+ it 'displays the list of files that are being uploaded' do
+ expect(output).to include "2 file(s) to upload: new_gemspec.gemspec, modified_lockfile.lock"
+ end
+
+ it 'makes a request to Gemnasium to upload needed files' do
+ expect(WebMock).to have_requested(:post, api_url('/api/v1/profiles/tech-angels/projects/gemnasium-gem/dependency_files/upload'))
+ .with(:body => hash_to_upload.to_json,
+ :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ end
+
+ it 'informs the user of added file' do
+ expect(output).to include 'Added dependency files: ["new_gemspec.gemspec"]'
+ end
+
+ it 'informs the user of updated file' do
+ expect(output).to include 'Updated dependency files: ["modified_lockfile.lockfile"]'
+ end
+
+ it 'informs the user of unchanged file' do
+ expect(output).to_not include 'Unchanged dependency files: []'
+ end
+
+ it 'informs the user of unsupported file' do
+ expect(output).to_not include 'Unsupported dependency files: []'
+ end
+ end
+ end
+ end
+ end
+
+ describe 'create_project' do
+ context 'for an already existing project' do
+ before { stub_config({ project_name: 'existing_project' }) }
+
+ context 'without overwrite option' do
+ it 'quit the program with an error' do
+ expect{ Gemnasium.create_project({ project_path: project_path }) }.to raise_error { |e|
+ expect(e).to be_kind_of SystemExit
+ expect(error_output).to include "The project `#{Gemnasium.config.project_name}` already exists for the profile `#{Gemnasium.config.profile_name}`."
+ }
+ end
+ end
+
+ context 'with overwrite option' do
+ before { Gemnasium.create_project({ project_path: project_path, overwrite_attr: true }) }
+
+ it 'issues the correct request' do
+ expect(WebMock).to have_requested(:post, api_url("/api/v1/profiles/tech-angels/projects"))
+ .with(:body => {name: "existing_project", privacy: "private", branch: "master", overwrite_attributes: true},
+ :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ end
+
+ it 'displays a confirmation message' do
+ expect(output).to include 'Private project `existing_project` successfully created for tech-angels.'
+ expect(output).to include 'Remaining private slots for this profile: 9001'
+ end
+ end
+ end
+
+ context 'for an inexistant project' do
+ before { Gemnasium.create_project({ project_path: project_path }) }
+
+ it 'issues the correct request' do
+ expect(WebMock).to have_requested(:post, api_url("/api/v1/profiles/tech-angels/projects"))
+ .with(:body => {name: "gemnasium-gem", privacy: "private", branch: "master"},
+ :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ end
+
+ it 'displays a confirmation message' do
+ expect(output).to include 'Private project `gemnasium-gemn` successfully created for tech-angels.'
+ expect(output).to include 'Remaining private slots for this profile: 9001'
+ end
+ end
+ end
+
+ describe 'install' do
+ after { FileUtils.rm_r "#{project_path}/config" }
+
+ context 'if config file already exists' do
+ before do
+ FileUtils.mkdir_p "#{project_path}/config"
+ FileUtils.touch("#{project_path}/config/gemnasium.yml")
+
+ Gemnasium.install({ project_path: project_path })
+ end
+
+ it 'informs the user that the file already exists' do
+ expect(output).to include "The file #{project_path}/config/gemnasium.yml already exists"
+ end
+ end
+
+ context 'if config file does not exist' do
+ context 'neither do the config folder' do
+ before do
+ FileUtils.touch "#{project_path}/.gitignore"
+ Gemnasium.install({ project_path: project_path })
+ end
+ after { FileUtils.rm "#{project_path}/.gitignore" }
+
+ it 'creates the config folder' do
+ expect(File.exists? "#{project_path}/config").to be_true
+ end
+
+ it 'informs the user that the folder has been created' do
+ expect(output).to include "Creating config directory"
+ end
+
+ it_should_behave_like 'an installed file' do
+ let(:template_path) { File.expand_path("#{File.dirname(__FILE__)}../../lib/templates/gemnasium.yml") }
+ let(:target_path) { "#{project_path}/config/gemnasium.yml" }
+ end
+
+ it 'adds the config file to the .gitignore' do
+ expect(output).to include "Configuration file added to your project's .gitignore."
+ expect(File.open("#{project_path}/.gitignore").read()).to include "# Gemnasium gem configuration file\nconfig/gemnasium.yml"
+ end
+
+ it 'asks the user to fill the config file' do
+ expect(output).to include 'Please fill configuration file with accurate values.'
+ end
+ end
+
+ context 'with an already existing config folder' do
+ before do
+ FileUtils.mkdir_p "#{project_path}/config"
+ Gemnasium.install({ project_path: project_path })
+ end
+
+ it 'does not inform the user that the config folder has been created' do
+ expect(output).to_not include "Creating config directory"
+ end
+
+ it_should_behave_like 'an installed file' do
+ let(:template_path) { File.expand_path("#{File.dirname(__FILE__)}../../lib/templates/gemnasium.yml") }
+ let(:target_path) { "#{project_path}/config/gemnasium.yml" }
+ end
+
+ it 'asks the user to fill the config file' do
+ expect(output).to include 'Please fill configuration file with accurate values.'
+ end
+ end
+ end
+
+ context 'with git option' do
+ context 'for a non git repo' do
+ before { Gemnasium.install({ project_path: project_path, install_git_hook: true }) }
+
+ it 'informs the user that the target project is not a git repository' do
+ expect(output).to include "#{project_path} is not a git repository. Try to run `git init`."
+ end
+
+ it 'does not install the hook' do
+ expect(File.exists? "#{project_path}/.git/hooks/post-commit").to eql false
+ end
+ end
+
+ context 'for a git repo' do
+ before { FileUtils.mkdir_p "#{project_path}/.git/hooks" }
+ after { FileUtils.rm_r "#{project_path}/.git" }
+
+ context 'if the hook already exists' do
+ before do
+ FileUtils.touch("#{project_path}/.git/hooks/post-commit")
+ Gemnasium.install({ project_path: project_path, install_git_hook: true })
+ end
+
+ it 'informs the user that a post-commit hook already exists.' do
+ expect(output).to include "The file #{project_path}/.git/hooks/post-commit already exists"
+ end
+ end
+
+ context 'if the hook does not exist' do
+ before { Gemnasium.install({ project_path: project_path, install_git_hook: true }) }
+
+ it_should_behave_like 'an installed file' do
+ let(:template_path) { File.expand_path("#{File.dirname(__FILE__)}../../lib/templates/post-commit") }
+ let(:target_path) { "#{project_path}/.git/hooks/post-commit" }
+ end
+ end
+ end
+ end
+
+ context 'with rake option' do
+ context 'for a repo without Rakefile' do
+ before { Gemnasium.install({ project_path: project_path, install_rake_task: true }) }
+
+ it 'informs the user that the target repo does not contain Rakefile' do
+ expect(output).to include "Rakefile not found."
+ end
+
+ it 'does not install the task' do
+ expect(File.exists? "#{project_path}/lib/tasks/gemnasium.rake").to eql false
+ end
+ end
+
+ context 'for a project with Rakefile' do
+ before { FileUtils.touch("#{project_path}/Rakefile") }
+ after do
+ FileUtils.rm "#{project_path}/Rakefile"
+ FileUtils.rm_r "#{project_path}/lib"
+ end
+
+ context 'without /lib/tasks foler' do
+ before { Gemnasium.install({ project_path: project_path, install_rake_task: true }) }
+
+ it 'creates the /lib/tasks folder' do
+ expect(File.exists? "#{project_path}/lib/tasks").to be_true
+ end
+
+ it 'informs the user that the folder has been created' do
+ expect(output).to include "Creating lib/tasks directory"
+ end
+
+ it_should_behave_like 'an installed file' do
+ let(:template_path) { File.expand_path("#{File.dirname(__FILE__)}../../lib/templates/gemnasium.rake") }
+ let(:target_path) { "#{project_path}/lib/tasks/gemnasium.rake" }
+ end
+
+ it 'informs the user on how to use the rake tasks' do
+ expect(output).to include 'Usage:'
+ expect(output).to include "\trake gemnasium:push \t\t- to push your dependency files"
+ expect(output).to include "\trake gemnasium:create \t\t- to create your project on Gemnasium"
+ expect(output).to include "\trake gemnasium:create:force - to overwrite already existing Gemnasium project attributes"
+ end
+ end
+
+ context 'with existing /lib/tasks foler' do
+ before { FileUtils.mkdir_p "#{project_path}/lib/tasks" }
+
+ context 'if the task file already exists' do
+ before do
+ FileUtils.touch("#{project_path}/lib/tasks/gemnasium.rake")
+ Gemnasium.install({ project_path: project_path, install_rake_task: true })
+ end
+
+ it 'informs the user that the rake task already exists.' do
+ expect(output).to include "The file #{project_path}/lib/tasks/gemnasium.rake already exists"
+ end
+ end
+
+ context 'if the task file does not exist' do
+ before { Gemnasium.install({ project_path: project_path, install_rake_task: true }) }
+
+ it_should_behave_like 'an installed file' do
+ let(:template_path) { File.expand_path("#{File.dirname(__FILE__)}../../lib/templates/gemnasium.rake") }
+ let(:target_path) { "#{project_path}/lib/tasks/gemnasium.rake" }
+ end
+
+ it 'informs the user on how to use the rake tasks' do
+ expect(output).to include 'Usage:'
+ expect(output).to include "\trake gemnasium:push \t\t- to push your dependency files"
+ expect(output).to include "\trake gemnasium:create \t\t- to create your project on Gemnasium"
+ expect(output).to include "\trake gemnasium:create:force - to overwrite already existing Gemnasium project attributes"
+ end
+ end
+ end
+ end
+ end
+ end
+end
8 spec/spec_helper.rb
@@ -0,0 +1,8 @@
+require 'gemnasium'
+require 'gemnasium/options'
+require 'rspec'
+require 'webmock/rspec'
+# Load support files
+Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
+
+WebMock.disable_net_connect!
73 spec/support/gemnasium_helper.rb
@@ -0,0 +1,73 @@
+def api_url(path)
+ config = Gemnasium.config
+ protocol = config.use_ssl ? "https://" : "http://"
+
+ "#{protocol}X:#{config.api_key}@#{config.site}#{path}"
+end
+
+def stub_config(options = {})
+ stubbed_config = double("Gemnasium::Configuration")
+ stubbed_config.stub(:site).and_return('gemnasium.com')
+ stubbed_config.stub(:use_ssl).and_return(true)
+ stubbed_config.stub(:api_key).and_return('test_api_key')
+ stubbed_config.stub(:api_version).and_return('v1')
+ stubbed_config.stub(:profile_name).and_return('tech-angels')
+ stubbed_config.stub(:project_name).and_return(options[:project_name] || 'gemnasium-gem')
+ stubbed_config.stub(:project_visibility).and_return('private')
+ stubbed_config.stub(:project_branch).and_return('master')
+
+ Gemnasium.stub(:config).and_return(stubbed_config)
+ Gemnasium.stub(:load_config).and_return(stubbed_config)
+end
+
+def stub_requests
+ config = Gemnasium.config
+ # Push requests
+ stub_request(:post, api_url("/api/#{config.api_version}/profiles/#{config.profile_name}/projects/up_to_date_project/dependency_files/compare"))
+ .with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ .to_return(:status => 200,
+ :body => '{ "to_upload": [], "deleted": [] }',
+ :headers => {'Content-Type'=>'application/json'})
+ stub_request(:post, api_url("/api/#{config.api_version}/profiles/#{config.profile_name}/projects/gemnasium-gem/dependency_files/compare"))
+ .with(:body => '{"new_gemspec.gemspec":"gemspec_sha1_hash","modified_lockfile.lock":"lockfile_sha1_hash","Gemfile_unchanged.lock":"gemfile_sha1_hash"}',
+ :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ .to_return(:status => 200,
+ :body => '{ "to_upload": ["new_gemspec.gemspec", "modified_lockfile.lock"], "deleted": ["old_dependency_file"] }',
+ :headers => {'Content-Type'=>'application/json'})
+ stub_request(:post, api_url("/api/#{config.api_version}/profiles/#{config.profile_name}/projects/gemnasium-gem/dependency_files/upload"))
+ .with(:body => '[{"filename":"new_gemspec.gemspec","sha":"gemspec_sha1_hash","content":"stubbed gemspec content"},{"filename":"modified_lockfile.lock","sha":"lockfile_sha1_hash","content":"stubbed lockfile content"}]',
+ :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ .to_return(:status => 200,
+ :body => '{ "added": ["new_gemspec.gemspec"], "updated": ["modified_lockfile.lockfile"], "unchanged": [], "unsupported": [] }',
+ :headers => {})
+ # Create requests
+ stub_request(:post, api_url("/api/#{config.api_version}/profiles/#{config.profile_name}/projects"))
+ .with(:body => '{"name":"gemnasium-gem","privacy":"private","branch":"master"}',
+ :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ .to_return(:status => 200,
+ :body => "{ \"is_private\": \"private\", \"name\": \"gemnasium-gemn\", \"profile\": \"#{config.profile_name}\", \"remaining_slot\": 9001 }",
+ :headers => {'Content-Type'=>'application/json'})
+ stub_request(:post, api_url("/api/#{config.api_version}/profiles/#{config.profile_name}/projects"))
+ .with(:body => '{"name":"existing_project","privacy":"private","branch":"master"}',
+ :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ .to_return do |request|
+ request_body = JSON.parse(request.body)
+ {
+ :status => 422,
+ :body => { error: 'project_already_exists', message: "The project `#{request_body['name']}` already exists for the profile `#{config.profile_name}`." }.to_json,
+ :headers => { 'Content-Type'=>'application/json' }
+ }
+ end
+ stub_request(:post, api_url("/api/#{config.api_version}/profiles/#{config.profile_name}/projects"))
+ .with(:body => '{"name":"existing_project","privacy":"private","branch":"master","overwrite_attributes":true}',
+ :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ .to_return(:status => 200,
+ :body => "{ \"is_private\": \"private\", \"name\": \"existing_project\", \"profile\": \"#{config.profile_name}\", \"remaining_slot\": 9001 }",
+ :headers => {'Content-Type'=>'application/json'})
+
+ # Connection model's test requests
+ stub_request(:get, api_url('/test_path'))
+ .with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+ stub_request(:post, api_url('/test_path'))
+ .with(:body => {"foo"=>"bar"}, :headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json'})
+end

0 comments on commit 8613024

Please sign in to comment.
Something went wrong with that request. Please try again.