codefumes 0.1.10 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +19 -0
- data/Gemfile.lock +135 -0
- data/History.txt +12 -0
- data/LICENSE +20 -0
- data/Manifest.txt +40 -19
- data/README.txt +11 -29
- data/Rakefile +15 -10
- data/bin/fumes +214 -0
- data/config/website.yml +2 -0
- data/cucumber.yml +2 -0
- data/features/claiming_a_project.feature +46 -0
- data/features/deleting_a_project.feature +32 -0
- data/features/releasing_a_project.feature +50 -0
- data/features/step_definitions/cli_steps.rb +98 -0
- data/features/step_definitions/common_steps.rb +168 -0
- data/features/step_definitions/filesystem_steps.rb +19 -0
- data/features/storing_user_api_key.feature +41 -0
- data/features/support/common.rb +29 -0
- data/features/support/env.rb +24 -0
- data/features/support/matchers.rb +11 -0
- data/features/synchronizing_repository_with_project.feature +33 -0
- data/lib/codefumes.rb +10 -8
- data/lib/codefumes/api.rb +20 -11
- data/lib/codefumes/api/build.rb +139 -0
- data/lib/codefumes/api/claim.rb +74 -0
- data/lib/codefumes/api/commit.rb +150 -0
- data/lib/codefumes/api/payload.rb +93 -0
- data/lib/codefumes/api/project.rb +158 -0
- data/lib/codefumes/cli_helpers.rb +54 -0
- data/lib/codefumes/config_file.rb +3 -2
- data/lib/codefumes/errors.rb +21 -0
- data/lib/codefumes/exit_codes.rb +10 -0
- data/lib/codefumes/harvester.rb +113 -0
- data/lib/codefumes/quick_build.rb +43 -0
- data/lib/codefumes/quick_metric.rb +20 -0
- data/lib/codefumes/source_control.rb +137 -0
- data/lib/integrity_notifier/codefumes.haml +11 -0
- data/lib/integrity_notifier/codefumes.rb +62 -0
- data/spec/codefumes/{build_spec.rb → api/build_spec.rb} +14 -24
- data/spec/codefumes/{claim_spec.rb → api/claim_spec.rb} +42 -3
- data/spec/codefumes/{commit_spec.rb → api/commit_spec.rb} +34 -24
- data/spec/codefumes/api/payload_spec.rb +148 -0
- data/spec/codefumes/api/project_spec.rb +286 -0
- data/spec/codefumes/api_spec.rb +38 -15
- data/spec/codefumes/config_file_spec.rb +69 -13
- data/spec/codefumes/harvester_spec.rb +118 -0
- data/spec/codefumes/source_control_spec.rb +199 -0
- data/spec/codefumes_service_helpers.rb +23 -19
- data/spec/fixtures/sample_project_dirs/no_scm/description +4 -0
- data/spec/spec_helper.rb +1 -0
- data/tasks/cucumber.rake +11 -0
- metadata +145 -60
- data/bin/cf_claim_project +0 -9
- data/bin/cf_release_project +0 -10
- data/bin/cf_store_credentials +0 -10
- data/lib/cf_claim_project/cli.rb +0 -95
- data/lib/cf_release_project/cli.rb +0 -76
- data/lib/cf_store_credentials/cli.rb +0 -50
- data/lib/codefumes/build.rb +0 -131
- data/lib/codefumes/claim.rb +0 -57
- data/lib/codefumes/commit.rb +0 -144
- data/lib/codefumes/payload.rb +0 -103
- data/lib/codefumes/project.rb +0 -129
- data/spec/cf_claim_project/cli_spec.rb +0 -17
- data/spec/cf_release_project/cli_spec.rb +0 -41
- data/spec/cf_store_credentials/cli_spec.rb +0 -28
- data/spec/codefumes/payload_spec.rb +0 -155
- data/spec/codefumes/project_spec.rb +0 -274
@@ -0,0 +1,93 @@
|
|
1
|
+
module CodeFumes
|
2
|
+
module API
|
3
|
+
# Payloads are intended to simplify sending up large amounts of
|
4
|
+
# content at one time. For example, when sending up the entire
|
5
|
+
# history of a repository, making a POST request for each commit would
|
6
|
+
# require a very large number of requests. Using a Payload object
|
7
|
+
# allows larger amounts of content to be saved at one time,
|
8
|
+
# significantly reducing the number of requests made.
|
9
|
+
class Payload
|
10
|
+
PAYLOAD_CHARACTER_LIMIT = 4000 #:nodoc:
|
11
|
+
|
12
|
+
attr_reader :project, :created_at
|
13
|
+
|
14
|
+
# +:commit_data+ should be a list of commits and associated data.
|
15
|
+
# An example would be:
|
16
|
+
#
|
17
|
+
# [{:identifier => "commit_identifer", :files_affected => 3,
|
18
|
+
# :custom_attributes => {:any_metric_you_want => "value"}}]
|
19
|
+
def initialize(project, commit_data)
|
20
|
+
@project = project
|
21
|
+
@content = {:commits => commit_data}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Saves instance to CodeFumes.com. After a successful save, the
|
25
|
+
# +created_at+ attribute will be populated with the timestamp the
|
26
|
+
# Payload was created.
|
27
|
+
#
|
28
|
+
# Returns +true+ if the Payload does not contain any content to be
|
29
|
+
# saved or the request was successful.
|
30
|
+
#
|
31
|
+
# Returns +false+ if the request failed.
|
32
|
+
def save
|
33
|
+
return true if empty_payload?
|
34
|
+
response = API.post("/projects/#{@project.public_key}/payloads", :query => {:payload => @content}, :basic_auth => {:username => @project.public_key, :password => @project.private_key})
|
35
|
+
|
36
|
+
case response.code
|
37
|
+
when 201
|
38
|
+
@created_at = response['payload']['created_at']
|
39
|
+
true
|
40
|
+
else
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# +save+ requests are made with a standard POST request (not a
|
46
|
+
# multi-part POST), so the request size is limited by the
|
47
|
+
# application server. The current configuration on CodeFumes.com
|
48
|
+
# limits requests to approximately 8,000 bytes (a little over). In
|
49
|
+
# order to simplify dealing with these constraints, without
|
50
|
+
# requiring a multi-part POST request, +prepare+ can be used to
|
51
|
+
# "chunk" up the data into Payloads which do not exceed this limit.
|
52
|
+
#
|
53
|
+
# Returns collection of payload objects which fall into the
|
54
|
+
# constraints of a individual payload (ie: length of raw request,
|
55
|
+
# et cetera).
|
56
|
+
#--
|
57
|
+
# TODO: Clean up how the size of the request is constrained, this
|
58
|
+
# is pretty hackish right now (basically guesses how many
|
59
|
+
# characters would be added when HTTParty wraps the content in XML.)
|
60
|
+
def self.prepare(project, commit_data = {})
|
61
|
+
return [] if commit_data.nil? || commit_data.empty?
|
62
|
+
raw_payload = commit_data.dup
|
63
|
+
|
64
|
+
content = raw_payload[:commits]
|
65
|
+
initial_chunks = {:on_deck => [], :prepared => []}
|
66
|
+
|
67
|
+
# TODO: Clean this up
|
68
|
+
chunked = content.inject(initial_chunks) do |chunks, new_commit|
|
69
|
+
if chunks[:on_deck].to_s.length + new_commit.to_s.length >= PAYLOAD_CHARACTER_LIMIT
|
70
|
+
chunks[:prepared] << chunks[:on_deck]
|
71
|
+
chunks[:on_deck] = [new_commit]
|
72
|
+
elsif new_commit == content.last
|
73
|
+
chunks[:on_deck] << new_commit
|
74
|
+
chunks[:prepared] << chunks[:on_deck]
|
75
|
+
chunks[:on_deck] = []
|
76
|
+
else
|
77
|
+
chunks[:on_deck] << new_commit
|
78
|
+
end
|
79
|
+
chunks
|
80
|
+
end
|
81
|
+
|
82
|
+
chunked[:prepared].map do |raw_content|
|
83
|
+
Payload.new(project, raw_content)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def empty_payload?
|
89
|
+
@content.nil? || @content.empty? || @content[:commits].nil? || @content[:commits].blank?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module CodeFumes
|
2
|
+
module API
|
3
|
+
# A Project encapsulates the concept of a project on the CodeFumes.com
|
4
|
+
# website. Each project has a public key, private key, and can have a
|
5
|
+
# name defined. Projects are also associated with a collection of
|
6
|
+
# commits from a repository.
|
7
|
+
class Project
|
8
|
+
attr_reader :private_key, :short_uri, :community_uri, :api_uri, :build_status
|
9
|
+
attr_accessor :name, :public_key
|
10
|
+
|
11
|
+
def initialize(public_key=nil, private_key = nil, options = {})
|
12
|
+
@public_key = public_key
|
13
|
+
@private_key = private_key
|
14
|
+
@name = options['name'] || options[:name]
|
15
|
+
@short_uri = options['short_uri'] || options[:short_uri]
|
16
|
+
@community_uri = options['community_uri'] || options[:community_uri]
|
17
|
+
@api_uri = options['api_uri'] || options[:api_uri]
|
18
|
+
@build_status = options['build_status'] || options[:build_status]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Creates new project
|
22
|
+
def self.create
|
23
|
+
response = API.post('/projects')
|
24
|
+
|
25
|
+
case response.code
|
26
|
+
when 201
|
27
|
+
new.reinitialize_from_hash!(response['project'])
|
28
|
+
else
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Deletes project from the website. You must have both the +public_key+
|
34
|
+
# and +private_key+ of a project in order to delete it.
|
35
|
+
#
|
36
|
+
# Returns +true+ if the request succeeded.
|
37
|
+
#
|
38
|
+
# Returns +false+ if the request failed.
|
39
|
+
def delete
|
40
|
+
if public_key.nil? || private_key.nil?
|
41
|
+
msg = "You must have both the private key & public key of a project in order to delete it. (currently: {:private_key => '#{private_key.to_s}', :public_key => '#{public_key.to_s}'}"
|
42
|
+
raise Errors::InsufficientCredentials, msg
|
43
|
+
end
|
44
|
+
|
45
|
+
response = destroy!
|
46
|
+
case response.code
|
47
|
+
when 200
|
48
|
+
return true
|
49
|
+
else
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Attempts to save current state of project to CodeFumes.
|
55
|
+
#
|
56
|
+
# Returns +true+ if the request succeeded.
|
57
|
+
#
|
58
|
+
# Returns +false+ if the request failed.
|
59
|
+
def save
|
60
|
+
response = API.put("/projects/#{public_key}", :query => {:project => {:name => name}},
|
61
|
+
:basic_auth => {:username => public_key, :password => private_key})
|
62
|
+
|
63
|
+
case response.code
|
64
|
+
when 200
|
65
|
+
reinitialize_from_hash!(response['project'])
|
66
|
+
true
|
67
|
+
else
|
68
|
+
false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Serializes a Project instance to a format compatible with the
|
73
|
+
# CodeFumes config file.
|
74
|
+
def to_config
|
75
|
+
project_attributes = {:api_uri => api_uri, :short_uri => short_uri}
|
76
|
+
project_attributes[:private_key] = private_key unless private_key.nil?
|
77
|
+
{public_key.to_sym => project_attributes}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Searches website for project with the supplied public key.
|
81
|
+
#
|
82
|
+
# Returns a Project instance if the project exists and is available,
|
83
|
+
# to the user making the request.
|
84
|
+
#
|
85
|
+
# Returns +nil+ in all other cases.
|
86
|
+
def self.find(public_key)
|
87
|
+
return nil if public_key.nil? || public_key.to_s.empty?
|
88
|
+
response = API.get("/projects/#{public_key}")
|
89
|
+
case response.code
|
90
|
+
when 200
|
91
|
+
project = Project.new
|
92
|
+
project.reinitialize_from_hash!(response['project'])
|
93
|
+
else
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Attempts to claim "ownership" of the project using the API key
|
99
|
+
# defined in the "credentials" section of your CodeFumes config
|
100
|
+
# file.
|
101
|
+
#
|
102
|
+
# If you need to claim a project for a key that is not defined in
|
103
|
+
# your config file, refer to Claim#create.
|
104
|
+
#
|
105
|
+
# Returns true if the request is successful.
|
106
|
+
#
|
107
|
+
# Returns +false+ in all other cases.
|
108
|
+
def claim
|
109
|
+
Claim.create(self, ConfigFile.api_key)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Attempts to relinquish "ownership" of the project using the API key
|
113
|
+
# defined in the "credentials" section of your CodeFumes config
|
114
|
+
# file.
|
115
|
+
#
|
116
|
+
# If you need to relinquish ownership of a project for a key that is
|
117
|
+
# not defined in your config file, refer to Claim#delete.
|
118
|
+
#
|
119
|
+
# Returns true if the request is successful.
|
120
|
+
#
|
121
|
+
# Returns +false+ in all other cases.
|
122
|
+
def release
|
123
|
+
Claim.destroy(self, ConfigFile.api_key)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Overrides existing attributes with those supplied in +options+. This
|
127
|
+
# simplifies the process of updating an object's state when given a response
|
128
|
+
# from the CodeFumes API.
|
129
|
+
#
|
130
|
+
# Valid options are:
|
131
|
+
# * name
|
132
|
+
# * public_key
|
133
|
+
# * private_key
|
134
|
+
# * short_uri
|
135
|
+
# * community_uri
|
136
|
+
# * api_uri
|
137
|
+
# * build_status
|
138
|
+
#
|
139
|
+
# Returns +self+
|
140
|
+
def reinitialize_from_hash!(options = {}) #:nodoc:
|
141
|
+
@name = options['name'] || options[:name]
|
142
|
+
@public_key = options['public_key'] || options[:public_key]
|
143
|
+
@private_key = options['private_key'] || options[:private_key]
|
144
|
+
@short_uri = options['short_uri'] || options[:short_uri]
|
145
|
+
@community_uri = options['community_uri'] || options[:community_uri]
|
146
|
+
@api_uri = options['api_uri'] || options[:api_uri]
|
147
|
+
@build_status = options['build_status'] || options[:build_status]
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
private
|
153
|
+
def destroy!
|
154
|
+
API.delete("/projects/#{@public_key}", :basic_auth => {:username => @public_key, :password => @private_key})
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module CodeFumes
|
2
|
+
# Module with convenience methods used in 'fumes' command line executable
|
3
|
+
module CLIHelpers #nodoc
|
4
|
+
def public_keys_specified(options)
|
5
|
+
return ConfigFile.public_keys if options[:all]
|
6
|
+
[options[:public_key] || SourceControl.new('./').public_key]
|
7
|
+
end
|
8
|
+
|
9
|
+
def print_api_mode_notification
|
10
|
+
puts "NOTE: Sending all requests & data to non-production server! (#{API.base_uri})"
|
11
|
+
end
|
12
|
+
|
13
|
+
def issue_project_commands(message, public_keys, &block)
|
14
|
+
public_keys.each do |public_key|
|
15
|
+
print "#{message}...'#{public_key}': "
|
16
|
+
project = Project.find(public_key.to_s)
|
17
|
+
yield(project)
|
18
|
+
end
|
19
|
+
|
20
|
+
puts ""
|
21
|
+
puts "Done!"
|
22
|
+
end
|
23
|
+
|
24
|
+
def wrap_with_standard_feedback(project, &block)
|
25
|
+
if project.nil?
|
26
|
+
puts "Project Not Found."
|
27
|
+
elsif yield != true
|
28
|
+
puts "Denied."
|
29
|
+
else
|
30
|
+
puts "Success!"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def command_doesnt_use_api?(command)
|
35
|
+
[:'api-key'].include?(command.name)
|
36
|
+
end
|
37
|
+
|
38
|
+
# lifted from the Github gem
|
39
|
+
def has_launchy?(&block)
|
40
|
+
begin
|
41
|
+
gem 'launchy'
|
42
|
+
require 'launchy'
|
43
|
+
block.call
|
44
|
+
rescue Gem::LoadError
|
45
|
+
raise Errors::MissingLaunchyGem, "'launchy' gem required, but missing"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# lifted from the Github gem
|
50
|
+
def open_in_browser(url, &block)
|
51
|
+
has_launchy? {Launchy::Browser.new.visit url}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -67,9 +67,10 @@ module CodeFumes
|
|
67
67
|
|
68
68
|
# Returns a Hash representation of a specific project contained in
|
69
69
|
# the CodeFumes config file.
|
70
|
-
def options_for_project(
|
70
|
+
def options_for_project(project_or_key)
|
71
|
+
public_key = project_or_key.is_a?(Project) ? project_or_key.public_key : project_or_key
|
71
72
|
config = serialized
|
72
|
-
|
73
|
+
project_or_key && config[:projects] && config[:projects][public_key.to_sym] || {}
|
73
74
|
end
|
74
75
|
|
75
76
|
def public_keys
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module CodeFumes
|
2
|
+
module Errors #:nodoc:
|
3
|
+
class InsufficientCredentials < StandardError #:nodoc:
|
4
|
+
end
|
5
|
+
|
6
|
+
class UnsupportedScmToolError < StandardError #:nodoc:
|
7
|
+
end
|
8
|
+
|
9
|
+
class UnknownProjectError < StandardError #:nodoc:
|
10
|
+
end
|
11
|
+
|
12
|
+
class NoUserApiKeyError < ArgumentError #:nodoc:
|
13
|
+
end
|
14
|
+
|
15
|
+
class NoApiKeySpecified < ArgumentError #:nodoc:
|
16
|
+
end
|
17
|
+
|
18
|
+
class MissingLaunchyGem < Gem::LoadError #:nodoc:
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module CodeFumes
|
2
|
+
# Simple class responsible for creating a project on the CodeFumes
|
3
|
+
# website, storing project information in the CodeFumes config file,
|
4
|
+
# and synchronizing a repository's commit history with CodeFumes.com.
|
5
|
+
#
|
6
|
+
# NOTE: Technically this can be used by anything (obviously), but it
|
7
|
+
# was written with the intention of being used with the
|
8
|
+
# +harvest_repo_metrics+ script, and is essentially geared for that
|
9
|
+
# scenario.
|
10
|
+
class Harvester
|
11
|
+
attr_reader :path
|
12
|
+
DEFAULT_PATH = './' #:nodoc:
|
13
|
+
|
14
|
+
# Accepts the following options:
|
15
|
+
# * +:path+ - the path of the repository to gather information from
|
16
|
+
# (Defaults to './').
|
17
|
+
# * +:public_key+ - Sets the public key of the project. This
|
18
|
+
# property will be read from the CodeFumes config file if one
|
19
|
+
# exists for the repository supplied at +:path+.
|
20
|
+
# * +:private_key+ - Sets the private key of the project. This
|
21
|
+
# property will be read from the CodeFumes config file if on
|
22
|
+
# exists for the repository supplied at +:path+.
|
23
|
+
# * +:name+ - Sets the name of the project on the CodeFumes site
|
24
|
+
#
|
25
|
+
# Note:
|
26
|
+
# Neither the +public_key+ nor +private_key+ is supported
|
27
|
+
# when creating a new project, but is used when updating an
|
28
|
+
# existing one. This prevents custom public keys from being
|
29
|
+
# created, but allows the user to share the public/private
|
30
|
+
# keys of a project with other users, or use them on other
|
31
|
+
# machines.
|
32
|
+
def initialize(passed_in_options = {})
|
33
|
+
options = passed_in_options.dup
|
34
|
+
@path = File.expand_path(options.delete(:path) || DEFAULT_PATH)
|
35
|
+
@repository = SourceControl.new(@path)
|
36
|
+
|
37
|
+
public_key = options[:public_key] || @repository.public_key
|
38
|
+
private_key = options[:private_key] || @repository.private_key
|
39
|
+
@project = initialize_project(public_key, private_key)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Creates or updates a project information on the CodeFumes site,
|
43
|
+
# synchronizes the repository's commit history, and prints the
|
44
|
+
# results to STDOUT.
|
45
|
+
#
|
46
|
+
# Returns a Hash containing the keys :successful_count and :total_count
|
47
|
+
# if the process succeeded and updates were posted to the server with
|
48
|
+
# their associated values.
|
49
|
+
#
|
50
|
+
# Returns and empty Hash if the local repository is in sync with the
|
51
|
+
# server and no updates were posted.
|
52
|
+
#
|
53
|
+
# Returns +false+ if the process failed.
|
54
|
+
def publish_data!
|
55
|
+
if @project.save
|
56
|
+
store_public_key_in_repository
|
57
|
+
update_codefumes_config_file
|
58
|
+
generate_and_save_payload || {}
|
59
|
+
else
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the CodeFumes public key of the project that is located
|
65
|
+
# at the supplied path.
|
66
|
+
def public_key
|
67
|
+
@project.public_key
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the CodeFumes private key of the project that is located
|
71
|
+
# at the supplied path.
|
72
|
+
def private_key
|
73
|
+
@project.private_key
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns the CodeFumes 'short uri' of the project that is located
|
77
|
+
# at the supplied path. The 'short uri' is similar to a Tiny URL for
|
78
|
+
# a project.
|
79
|
+
def short_uri
|
80
|
+
@project.short_uri
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
#TODO: Smarten this up a bit.
|
85
|
+
# - Handle nils more cleanly, etc.
|
86
|
+
# - Don't #create if _something_ was supplied. Verify it was valid or exit.
|
87
|
+
def initialize_project(public_key = nil, private_key = nil)
|
88
|
+
return Project.create if public_key.nil? || public_key.empty?
|
89
|
+
|
90
|
+
msg = "Project public key provided was not found via the API (supplied '#{public_key}')."
|
91
|
+
Project.find(public_key) || raise(Errors::UnknownProjectError, msg)
|
92
|
+
end
|
93
|
+
|
94
|
+
def store_public_key_in_repository
|
95
|
+
@repository.store_public_key(@project.public_key)
|
96
|
+
end
|
97
|
+
|
98
|
+
def update_codefumes_config_file
|
99
|
+
ConfigFile.save_project(@project)
|
100
|
+
end
|
101
|
+
|
102
|
+
def generate_and_save_payload
|
103
|
+
payload = @repository.payload(Commit.latest_identifier(@project), "HEAD")
|
104
|
+
if payload.empty?
|
105
|
+
nil
|
106
|
+
else
|
107
|
+
payloads = Payload.prepare(@project, payload)
|
108
|
+
successful_requests = payloads.select {|payload| payload.save == true}
|
109
|
+
{:successful_count => successful_requests.size, :total_count => payloads.size}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|