codefumes 0.1.10 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/Gemfile +19 -0
  2. data/Gemfile.lock +135 -0
  3. data/History.txt +12 -0
  4. data/LICENSE +20 -0
  5. data/Manifest.txt +40 -19
  6. data/README.txt +11 -29
  7. data/Rakefile +15 -10
  8. data/bin/fumes +214 -0
  9. data/config/website.yml +2 -0
  10. data/cucumber.yml +2 -0
  11. data/features/claiming_a_project.feature +46 -0
  12. data/features/deleting_a_project.feature +32 -0
  13. data/features/releasing_a_project.feature +50 -0
  14. data/features/step_definitions/cli_steps.rb +98 -0
  15. data/features/step_definitions/common_steps.rb +168 -0
  16. data/features/step_definitions/filesystem_steps.rb +19 -0
  17. data/features/storing_user_api_key.feature +41 -0
  18. data/features/support/common.rb +29 -0
  19. data/features/support/env.rb +24 -0
  20. data/features/support/matchers.rb +11 -0
  21. data/features/synchronizing_repository_with_project.feature +33 -0
  22. data/lib/codefumes.rb +10 -8
  23. data/lib/codefumes/api.rb +20 -11
  24. data/lib/codefumes/api/build.rb +139 -0
  25. data/lib/codefumes/api/claim.rb +74 -0
  26. data/lib/codefumes/api/commit.rb +150 -0
  27. data/lib/codefumes/api/payload.rb +93 -0
  28. data/lib/codefumes/api/project.rb +158 -0
  29. data/lib/codefumes/cli_helpers.rb +54 -0
  30. data/lib/codefumes/config_file.rb +3 -2
  31. data/lib/codefumes/errors.rb +21 -0
  32. data/lib/codefumes/exit_codes.rb +10 -0
  33. data/lib/codefumes/harvester.rb +113 -0
  34. data/lib/codefumes/quick_build.rb +43 -0
  35. data/lib/codefumes/quick_metric.rb +20 -0
  36. data/lib/codefumes/source_control.rb +137 -0
  37. data/lib/integrity_notifier/codefumes.haml +11 -0
  38. data/lib/integrity_notifier/codefumes.rb +62 -0
  39. data/spec/codefumes/{build_spec.rb → api/build_spec.rb} +14 -24
  40. data/spec/codefumes/{claim_spec.rb → api/claim_spec.rb} +42 -3
  41. data/spec/codefumes/{commit_spec.rb → api/commit_spec.rb} +34 -24
  42. data/spec/codefumes/api/payload_spec.rb +148 -0
  43. data/spec/codefumes/api/project_spec.rb +286 -0
  44. data/spec/codefumes/api_spec.rb +38 -15
  45. data/spec/codefumes/config_file_spec.rb +69 -13
  46. data/spec/codefumes/harvester_spec.rb +118 -0
  47. data/spec/codefumes/source_control_spec.rb +199 -0
  48. data/spec/codefumes_service_helpers.rb +23 -19
  49. data/spec/fixtures/sample_project_dirs/no_scm/description +4 -0
  50. data/spec/spec_helper.rb +1 -0
  51. data/tasks/cucumber.rake +11 -0
  52. metadata +145 -60
  53. data/bin/cf_claim_project +0 -9
  54. data/bin/cf_release_project +0 -10
  55. data/bin/cf_store_credentials +0 -10
  56. data/lib/cf_claim_project/cli.rb +0 -95
  57. data/lib/cf_release_project/cli.rb +0 -76
  58. data/lib/cf_store_credentials/cli.rb +0 -50
  59. data/lib/codefumes/build.rb +0 -131
  60. data/lib/codefumes/claim.rb +0 -57
  61. data/lib/codefumes/commit.rb +0 -144
  62. data/lib/codefumes/payload.rb +0 -103
  63. data/lib/codefumes/project.rb +0 -129
  64. data/spec/cf_claim_project/cli_spec.rb +0 -17
  65. data/spec/cf_release_project/cli_spec.rb +0 -41
  66. data/spec/cf_store_credentials/cli_spec.rb +0 -28
  67. data/spec/codefumes/payload_spec.rb +0 -155
  68. 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(public_key)
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
- public_key && config[:projects] && config[:projects][public_key.to_sym] || {}
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,10 @@
1
+ module ExitCodes
2
+ SUCCESS = 0
3
+ UNSUPPORTED_SCM = 1
4
+ PROJECT_NOT_FOUND = 2
5
+ NO_USER_CREDENTIALS = 3
6
+ INCORRECT_USER_CREDENTIALS = 4
7
+ NO_API_KEY_SPECIFIED = 5
8
+ MISSING_DEPENDENCY = 6
9
+ UNKNOWN = 100
10
+ 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